ES 必知必會(huì)

0、ES 基本概念

一個(gè)ES集群可以有多個(gè)節(jié)點(diǎn)構(gòu)成,一個(gè)節(jié)點(diǎn)就是一個(gè)ES服務(wù)實(shí)例。ES 集群角色:

  • 候選主節(jié)點(diǎn):只有是候選主節(jié)點(diǎn)才可以參與選舉投票,也只有候選主節(jié)點(diǎn)可以被選舉為主節(jié)點(diǎn)。
  • 主節(jié)點(diǎn):負(fù)責(zé)索引的添加、刪除,跟蹤哪些節(jié)點(diǎn)是群集的一部分,對分片進(jìn)行分配、收集集群中各節(jié)點(diǎn)的狀態(tài)等,穩(wěn)定的主節(jié)點(diǎn)對集群的健康是非常重要。
  • 數(shù)據(jù)節(jié)點(diǎn):負(fù)責(zé)對數(shù)據(jù)的增、刪、改、查、聚合等操作,數(shù)據(jù)的查詢和存儲(chǔ)都是由數(shù)據(jù)節(jié)點(diǎn)負(fù)責(zé),對機(jī)器的CPU,IO以及內(nèi)存的要求比較高,一般選擇高配置的機(jī)器作為數(shù)據(jù)節(jié)點(diǎn)。
  • 協(xié)調(diào)節(jié)點(diǎn):其本身不是通過設(shè)置來分配的,用戶的請求可以隨機(jī)發(fā)往任何一個(gè)節(jié)點(diǎn),并由該節(jié)點(diǎn)負(fù)責(zé)分發(fā)請求、收集結(jié)果等操作,而不需要主節(jié)點(diǎn)轉(zhuǎn)發(fā)。這種節(jié)點(diǎn)可稱之為協(xié)調(diào)節(jié)點(diǎn),集群中的任何節(jié)點(diǎn)都可以充當(dāng)協(xié)調(diào)節(jié)點(diǎn)的角色。每個(gè)節(jié)點(diǎn)之間都會(huì)保持聯(lián)系。

核心概念:

術(shù)語 描述 用法 數(shù)據(jù)庫對比概念
字段(Field) 用于表述每一個(gè)列的名字,字段是文檔的組成單元,包含字段名稱、字段屬性和字段內(nèi)容 例如商戶名,城市名就分別是一個(gè)字段
字段屬性(Attributes) 描述字段的屬性,例如城市名的屬性是一個(gè)字符串類型,不需要分詞等 用來描述字段的,包括是否建倒排、正排、分詞、值的類型(數(shù)字類型、字符串類型等) 類似字段的Varchar(16),int(20)是否index, primary_index等
文檔(document) 文檔是可搜索的結(jié)構(gòu)化數(shù)據(jù)單元,用于描述一整條記錄,由多個(gè)字段組成 例如上海天山西路的KFC商戶我們可以作為一個(gè)文檔 記錄行
索引(index) 用于描述多個(gè)行記錄的集合 例如把所有的商戶放在一個(gè)索引里
正排 文檔到字段對應(yīng)關(guān)系組成的鏈表,勻速可通過后臺構(gòu)建正排鏈表,doc1->id,type,create_time... 設(shè)置docvalues=true 行記錄
倒排 詞組到文檔的對應(yīng)關(guān)系組成的鏈表,勻速可搜索后會(huì)構(gòu)建倒排鏈表,term1->doc1,doc2,doc3; term2->doc1,doc2 設(shè)置index=true,如果為false,對應(yīng)的字段域?qū)⒉荒苓M(jìn)行檢索(即執(zhí)行各種Query后返回的結(jié)果為空,或者直接報(bào)錯(cuò)) 類似B+樹索引
Mysql不設(shè)置索引還是可以進(jìn)行查詢,但是ES不設(shè)置,查詢結(jié)果為空
召回 通過用戶查詢的關(guān)鍵詞進(jìn)行分詞,將分詞后的詞組通過查找倒排鏈表快速定位到文檔,這個(gè)過程稱為召回。 查詢過程
召回量 召回得到的文檔數(shù)為召回量,即totalhits 查詢返回的結(jié)果數(shù)
分片(Shard) 一個(gè)索引由多個(gè)分片組成,這些分片是索引里面的一部分,每一個(gè)分片都具備索引相同的數(shù)據(jù)結(jié)構(gòu) 一個(gè)索引分成N個(gè)shard,每一個(gè)Shard的內(nèi)容就是這個(gè)完整索引內(nèi)容的1/N 在數(shù)據(jù)庫里沒有類似的概念
副本(replicas) 副本就是對分片的復(fù)制,每個(gè)主分片都有一個(gè)或多個(gè)副本分片,當(dāng)主分片異常時(shí),副本可以提供數(shù)據(jù)的查詢等操作 主分片和對應(yīng)的副本分片是不會(huì)在同一個(gè)節(jié)點(diǎn)上的,避免數(shù)據(jù)的丟失,當(dāng)一個(gè)節(jié)點(diǎn)宕機(jī)時(shí)候,還可以通過副本查詢到數(shù)據(jù),副本分片數(shù)的最大值是 N-1(其中 N 為節(jié)點(diǎn)數(shù))。
段(Segment) 分片的組成單元,即多個(gè)段構(gòu)成一個(gè)分片,段是檢索的基本單元,所有的查詢/更新都是基于段來查詢的。
段合并 Lucene的刪除是標(biāo)記刪除,更新是先刪后增,隨著數(shù)據(jù)不斷的更新,一個(gè)分片中會(huì)累積很多段(這些段里存在很多已經(jīng)刪掉的文檔),段太多會(huì)導(dǎo)致查詢性能變慢,因此我們需要一個(gè)段合并的過程,將那些沒有用的數(shù)據(jù)清除,減少段的個(gè)數(shù)
1、如何保障ES高可用的?
  • Elasticsearch 高可用的核心是分片,并且每個(gè)分片都有主從之分。也就是說,萬一主分片崩潰了,還可以使用從分片,從而保證了最基本的可用性。
  • 而且 Elasticsearch 在寫入數(shù)據(jù)的過程中,為了保證高性能,都是寫到自己的 Buffer 里面,后面再刷新到磁盤上。所以為了降低數(shù)據(jù)丟失的風(fēng)險(xiǎn),Elasticsearch 還額外寫了一個(gè) Translog,它就類似于 MySQL 里的 redo log。后面 Elasticsearch 崩潰之后,可以利用 Translog 來恢復(fù)數(shù)據(jù)。
  • 利用消息隊(duì)列削峰
2、如何優(yōu)化ES查詢性能的?
  • 優(yōu)化分頁查詢: search_after 就類似于分庫分表中禁用跳頁查詢里面加入的 WHERE id > $max_id 這種極值過濾條件
  • 增大刷新間隔:把 index.refresh_interval 調(diào)大
  • 批量提交:單個(gè)消費(fèi) Kafka,改為批量消費(fèi)批量提交
  • 日志同步場景:按日志級別降低,優(yōu)先 error->warn->info
  • ES 索引只保留與查詢必要字段,其他回表查
  • 冷數(shù)據(jù)定期歸檔:按日期建索引,定期歸檔掉很久之前的索引(與業(yè)務(wù)共識)
  • 優(yōu)化 JVM:Elasicsearch 用的 CMS,而 CMS 在超過 8G 的堆上面,表現(xiàn)就比較差,可考慮切換為 G1 或 ZGC
  • 使用 Elasticsearch 的時(shí)候要把 swap 禁用,或者把 vm.swappness 設(shè)置得很小,也可以把 bootstrap.memory_lock 設(shè)置成 true。
  • 業(yè)務(wù)隔離:區(qū)分核心業(yè)務(wù)與非核心業(yè)務(wù),不要放到一個(gè)集群上
3、如何預(yù)估ES集群規(guī)格和容量?(了解即可)

參考:Elasticsearch集群規(guī)模和容量規(guī)劃的底層邏輯

3.1 磁盤容量預(yù)估

影響ES磁盤空間的因素有以下幾點(diǎn):

  • 副本數(shù)量:副本有利于增加數(shù)據(jù)的可靠性,但同時(shí)會(huì)增加存儲(chǔ)成本。默認(rèn)和建議的副本數(shù)量為1,對于部分可以承受異常情況導(dǎo)致數(shù)據(jù)丟失的場景,可考慮設(shè)置副本數(shù)量為0。
  • 索引開銷:除原始數(shù)據(jù)外,ES 需要存儲(chǔ)索引、列存數(shù)據(jù)等,在應(yīng)用編碼壓縮等技術(shù)后,一般膨脹10%。(_all等未計(jì)算)。
  • 操作系統(tǒng)預(yù)留:操作系統(tǒng)默認(rèn)會(huì)保留5%的文件系統(tǒng)供用戶處理關(guān)鍵流程、系統(tǒng)恢復(fù)以及磁盤碎片等。
  • ES內(nèi)部開銷:ES 占用約20%的磁盤空間,用于 segment 合并、ES Translog、日志等。
  • 安全閾值:通常至少預(yù)留15%的安全閾值,避免節(jié)點(diǎn)沒有充分時(shí)間進(jìn)行調(diào)整。

根據(jù)以上因素得到:最小磁盤總大小 = 源數(shù)據(jù)大小【例如業(yè)務(wù)數(shù)據(jù)庫表的物理大小】 * 3.4。
磁盤總大小 = 源數(shù)據(jù) * (1 + 副本數(shù)量) * (1 + 索引開銷) / (1 - 操作系統(tǒng)預(yù)留空間) / (1 - ES內(nèi)部開銷) / (1 - 安全閾值) = 源數(shù)據(jù) * (1 + 副本數(shù)量) * 1.7 = 源數(shù)據(jù) * 3.4

說明

  • 對于_all這項(xiàng)參數(shù),如果在業(yè)務(wù)使用上沒有必要,通常建議您禁止或者有選擇性地添加。
  • 對于需要開啟這個(gè)參數(shù)的索引,其開銷也會(huì)隨之增大。根據(jù)測試結(jié)果和使用經(jīng)驗(yàn),建議您在上述評估的基礎(chǔ)上額外增加一半的空間,即:磁盤總大小 = 源數(shù)據(jù) * (1 + 副本數(shù)) * 1.7 * (1 + 0.5) = 源數(shù)據(jù) * 5.1
3.2 分片數(shù)量評估

每個(gè) ES 索引被分為多個(gè)分片,數(shù)據(jù)按哈希算法打散到不同的分片中。由于索引分片的數(shù)量影響讀寫性能、故障恢復(fù)速度,且通常無法輕松更改,需要提前考慮。這里給出配置分片數(shù)量的一些常用建議:

  • 建議單個(gè)分片大小保持在30G左右,您可以據(jù)此初步確定 Index 的分片數(shù)量。分片不宜過大或過小:過大可能使 ES 的故障恢復(fù)速度變慢;過小可能導(dǎo)致非常多的分片,但因?yàn)槊總€(gè)分片使用一些數(shù)量的 CPU 和內(nèi)存,從而導(dǎo)致讀寫性能、內(nèi)存不足等問題
  • 在測試階段,可以根據(jù)每個(gè) Index 的實(shí)際大小、預(yù)期未來增長情況,適當(dāng)調(diào)整分片數(shù)量。
  • 當(dāng)分片數(shù)量超過數(shù)據(jù)節(jié)點(diǎn)數(shù)量時(shí),建議分片數(shù)量接近數(shù)據(jù)節(jié)點(diǎn)的整數(shù)倍,方便分片在所有數(shù)據(jù)節(jié)點(diǎn)均勻分布。
  • 對于日志、Metric 等場景中,建議按時(shí)間滾動(dòng)索引,持續(xù)滾動(dòng)產(chǎn)生新的 Index。方便在發(fā)現(xiàn)分片大小不合理時(shí),通過該功能及時(shí)調(diào)整分片數(shù)量。

例如,假設(shè)實(shí)例有5個(gè)數(shù)據(jù)節(jié)點(diǎn),Index 當(dāng)前大小為150GB,預(yù)期半年后增長50%。如果我們控制每個(gè)單分片為30GB,則大約需要150GB ×(1 + 50%)/ 30 ≈ 7個(gè)分片,考慮到有兩個(gè)數(shù)據(jù)節(jié)點(diǎn)支撐2/7的數(shù)據(jù)壓力,節(jié)點(diǎn)間壓力相對不均勻,我們把分片數(shù)量調(diào)整到10個(gè)。

3.3 集群規(guī)格評估

ES的單機(jī)規(guī)格可能會(huì)對集群的能力有所限制,因此在使用Elasticsearch服務(wù)前,首先需要對集群規(guī)格進(jìn)行評估,并根據(jù)評估結(jié)果進(jìn)行擴(kuò)容、升配等操作。本文檔根據(jù)測試結(jié)果和使用經(jīng)驗(yàn)給出如下建議。

  • 集群最大節(jié)點(diǎn)數(shù):集群最大節(jié)點(diǎn)數(shù) = 單節(jié)點(diǎn)CPU * 5。
  • 單節(jié)點(diǎn)最大承載的數(shù)據(jù)量:使用場景不同,單節(jié)點(diǎn)最大承載的數(shù)據(jù)量也會(huì)不同,具體如下。
    • 數(shù)據(jù)加速、查詢聚合等場景:單節(jié)點(diǎn)最大數(shù)據(jù)量 = 單節(jié)點(diǎn)Mem(G) * 10。
    • 日志寫入、離線分析等場景:單節(jié)點(diǎn)最大數(shù)據(jù)量 = 單節(jié)點(diǎn)Mem(G) * 50。
    • 通常情況:單節(jié)點(diǎn)最大數(shù)據(jù)量 = 單節(jié)點(diǎn)Mem(G) * 30。

ES平臺支持單節(jié)點(diǎn)的最大磁盤為2TB。ES平臺的cpu內(nèi)存規(guī)格和磁盤規(guī)格不是綁定關(guān)系,業(yè)務(wù)可以根據(jù)自身需求配置大磁盤或者小磁盤機(jī)器。申請節(jié)點(diǎn)時(shí)需要注意以下表格為數(shù)據(jù)量,而機(jī)器磁盤需要更大,以使得磁盤使用率在合理區(qū)間,最大不超過85%。

機(jī)器規(guī)格 最大節(jié)點(diǎn)數(shù) 單節(jié)點(diǎn)最大數(shù)據(jù)量(查詢) 單節(jié)點(diǎn)最大數(shù)據(jù)量(日志) 單節(jié)點(diǎn)最大數(shù)據(jù)量(通常)
8C 16G 40 160 GB 800 GB 480 GB
16C 32G 80 320 GB 1.6 TB 960 GB
32C 64G 160 640G 2 TB 2 TB
3.4 計(jì)算資源評估

ES 的計(jì)算資源主要消耗在寫入和查詢過程,而不同業(yè)務(wù)場景在寫入和查詢方面的復(fù)雜度不同、比重不同,導(dǎo)致計(jì)算資源相比存儲(chǔ)資源較難評估。但一般情況下,存儲(chǔ)資源會(huì)較早成為瓶頸,因此建議您優(yōu)先評估存儲(chǔ)資源量,然后參考 ES 節(jié)點(diǎn)類型 初步選擇計(jì)算資源,在測試過程中確認(rèn)計(jì)算資源是否足夠。下面針對幾種常見使用場景,介紹計(jì)算資源評估過程中的一些經(jīng)驗(yàn):

  • 日志場景:日志屬于典型的寫多讀少類場景,計(jì)算資源主要消耗在寫入過程中。我們在日志場景的經(jīng)驗(yàn)是:2核8GB內(nèi)存的資源最大可支持0.5萬次寫入/s的寫入能力,但注意不同業(yè)務(wù)場景可能有偏差。由于實(shí)例性能基本隨計(jì)算資源總量呈線性擴(kuò)容,您可以按實(shí)例資源總量估算寫入能力。例如8核32GB內(nèi)存的資源可支持2萬次寫入/s的寫入能力。
  • Metric 及 APM 等結(jié)構(gòu)化數(shù)據(jù)場景:這也是寫多讀少類場景,但相比日志場景計(jì)算資源消耗較小,2核8GB內(nèi)存的資源一般可支持1萬次寫入/s的寫入能力,您可參照日志場景線性擴(kuò)展的方式,評估不同規(guī)格實(shí)例的實(shí)際寫入能力。
  • 站內(nèi)搜索及應(yīng)用搜索等搜索場景:此類為讀多寫少類場景,計(jì)算資源主要消耗在查詢過程,由于查詢復(fù)雜度在不同使用場景差別非常大,計(jì)算資源也最難評估,建議您結(jié)合存儲(chǔ)資源初步選擇計(jì)算資源,然后在測試過程中驗(yàn)證、調(diào)整。
3.5 實(shí)例類型選擇及測試

在完成存儲(chǔ)、計(jì)算資源評估后,您可以初步參考 ES平臺節(jié)點(diǎn)規(guī)格 選擇實(shí)例類型,這里包含節(jié)點(diǎn)規(guī)格和節(jié)點(diǎn)數(shù)量兩方面。選擇實(shí)例類型的常用建議如下:

  • 建議您至少選擇3個(gè)master節(jié)點(diǎn),避免 ES 實(shí)例出現(xiàn)腦裂問題,保證 ES 實(shí)例具有較高的節(jié)點(diǎn)故障容錯(cuò)能力。腦裂:兩個(gè)節(jié)點(diǎn)同時(shí)認(rèn)為自己是唯一處于活動(dòng)狀態(tài)的服務(wù)器,從而出現(xiàn)爭用資源的情況。
  • 若您有非常大的存儲(chǔ)容量需求,建議選擇高規(guī)格的節(jié)點(diǎn),避免大量低規(guī)格節(jié)點(diǎn),這對大實(shí)例的性能、穩(wěn)定性等有較大好處。例如,若您有40核160GB內(nèi)存5TB存儲(chǔ)容量需求,建議選擇8核32GB內(nèi)存1TB × 5節(jié)點(diǎn)的實(shí)例。同理,當(dāng)您需要對實(shí)例擴(kuò)容時(shí),建議優(yōu)先進(jìn)行縱向擴(kuò)容,把節(jié)點(diǎn)擴(kuò)容到8核32GB或16核64GB的規(guī)格,然后再考慮橫向擴(kuò)容增加節(jié)點(diǎn)個(gè)數(shù)。
  • 當(dāng)完成實(shí)例類型的初步選擇后,您可以使用真實(shí)數(shù)據(jù)進(jìn)行測試,通過觀察 CPU 使用率、寫入指標(biāo)(性能、拒絕率)、查詢指標(biāo)(QPS、拒絕率)等監(jiān)控信息,進(jìn)一步確認(rèn)實(shí)例類型是否合適。另外,建議針對上述監(jiān)控信息配置告警,方便在線上使用時(shí),及時(shí)發(fā)現(xiàn)資源不足等問題。
4、ES 優(yōu)秀實(shí)踐(了解即可)
4.1 索引配置
  • 使用term查詢或terms查詢、terms聚合查詢,字段類型需配置為keyword類型,term查詢使用keyword類型相對于數(shù)值類型有3~5倍性能提升,大大減少cpu使用率);使用數(shù)值型(long/double等)term查詢性能較差而且cpu占用高
  • 字符串類型,如果需要分詞則選擇text,會(huì)對每個(gè)詞建立倒排索引,存儲(chǔ)成本較高,所以無需分詞得情況請使用keyword
  • 數(shù)值型Range或排序查詢,字段類型配置為數(shù)值型比keyword的字段類型效果更好,特別注意:排序使用數(shù)值字段,不要用默認(rèn)的“_id”字段排序,性能較差。
  • 數(shù)值類型,盡量選擇貼合實(shí)際數(shù)值大小得類型,比如價(jià)格,使用float或者double,不需要范圍查詢盡量使用keyword,在5.x數(shù)字類型的Term查詢性能相比2.x下降80%,CPU利用率也會(huì)飆升30%左右。
  • 索引主分片、索引字段類型必須在使用前就確定好,后期修改成本非常高(這兩項(xiàng)一旦配置后就無法修改,除非重建索引)!主分片數(shù)量太少影響擴(kuò)展性,數(shù)量太多段文件過多影響查詢性能,需要按業(yè)務(wù)實(shí)際情況合理分配
  • ES7以上版本建議顯式配置refresh_interval=1s以避免慢查詢。
  • 索引單個(gè)分片建議控制在30G左右,分片過大特別容易造成機(jī)器性能瓶頸,但也不能太小,建議單個(gè)分片大小需要保持在10GB - 50GB之間
  • 索引不要配過多數(shù)量的主分片,分片數(shù)量過多占用過多系統(tǒng)資源,增加主master壓力,影響查詢性能。集群的總分片數(shù)建議不要超過5000。
  • 索引分片文檔數(shù)上限為INT.MAX-128,也即:2,147,483,519,超過該值后分片無法分配,索引無法讀寫,對于預(yù)期文檔數(shù)量超二十一億的文檔,創(chuàng)建索引時(shí),主分片數(shù)必須>1。
  • 索引配置慢日志,當(dāng)出現(xiàn)慢查時(shí)可以分析定位具體問題查詢語句,分析慢查詢原因時(shí),使用profile功能定位具體問題
  • 對于terms過大的查詢,可以在索引定義中限制terms最大數(shù)量
4.2 查詢規(guī)則
  • 限制查詢字段長度,特別是wildcard、match pharse、prefix等查詢,稍有不慎就有可能把集群查掛
  • 對與前綴查詢(Prefix Query)、正則查詢(Regexp Query)、FuzzyQuery、通配符查詢(Wildcard Query),需要注意相關(guān)查詢詞不要超過32個(gè)字符,嚴(yán)禁使用前后通用匹配(類似"查詢詞"),這類查詢?nèi)绻樵冊~太大,在ES內(nèi)部會(huì)造成復(fù)雜的自動(dòng)機(jī)狀態(tài)計(jì)算,導(dǎo)致集群CPU飆滿,直至完全不可用
  • 避免使用嵌套類型nested,使用嵌套類型性能會(huì)慢10幾倍。ES不擅長進(jìn)行join處理,盡量把join邏輯處理放在客戶端代碼。
  • 避免使用父子關(guān)系,parent-child,使用父子類型性能會(huì)慢百倍以上。
  • 模糊查詢盡量使用分詞查詢(match query)替代大部分情況match分詞查詢能滿足模糊匹配的需求,盡量使用match或者match_query替代,大部分情況match query都能滿足全文檢索,同樣的數(shù)據(jù)和字段,match分詞查詢比wildcard查詢性能好1倍以上。
  • 使用wildcard屬性進(jìn)行替代原始類型
  • 控制聚合的數(shù)量,減少內(nèi)存的開銷。聚合查詢嵌套不要超過3層,控制聚合統(tǒng)計(jì)查詢的深度,深度越深,CPU越高、IO越多、性能越差。
  • 范圍查詢推薦使用整值型或者范圍類型字段,number類型底層BKD數(shù)據(jù)結(jié)構(gòu)更適合range查詢,性能更佳。
  • Range查詢掃描行數(shù)不要超過100000行
  • 盡量使用query-bool-filter, filter的緩存機(jī)制會(huì)使的檢索更快
  • 不要使用match All 查詢
  • 需要控制返回結(jié)果集,注意深度翻頁問題,建議size不要超過1000,最大值不超過10000,這是引發(fā)full gc常見的原因
  • 如果需要遍歷數(shù)據(jù),請使用Scroll查詢,如果對翻頁有實(shí)時(shí)查詢需求,可以使用SearchAfter
  • 控制返回字段,_source過濾,只返回業(yè)務(wù)相關(guān)的字段,減少IO開銷,如下,搜索返回結(jié)果中希望包含obj1和obj2開頭得字段
  • 別名查詢要定期清理或者解綁歷史索引。別名使用于定期按照模板創(chuàng)建的索引,比如檔期數(shù)據(jù)每個(gè)月建一個(gè)索引,查詢時(shí)使用同一個(gè)別名,比較靈活,別名查詢會(huì)遍歷所有關(guān)聯(lián)的索引,如果關(guān)聯(lián)的索引過多,會(huì)導(dǎo)致單次查詢遍歷非常多的分片,從而影響性能。
    • 要定期對不需要的歷史索引進(jìn)行解綁或者清理。
    • 別名綁定的索引不宜超過20個(gè)。
  • 查詢不要太復(fù)雜:Term/Terms查詢的入?yún)€(gè)數(shù)不要超過100個(gè);子查詢個(gè)數(shù)不要超過1024個(gè),查詢越復(fù)雜,性能越差,很容易導(dǎo)致集群崩掉。
  • 深分頁、多層聚合、嵌套、msearch/collapse都會(huì)嚴(yán)重影響查詢效率
  • 查詢結(jié)果無需返回score的情況下,優(yōu)先使用filter,查詢結(jié)果會(huì)被緩存,以達(dá)到提升集群整體查詢性能目的。過濾條件盡可能粗粒度,以實(shí)現(xiàn)緩存最大化(譬如range查按小時(shí)過濾,緩存的結(jié)果就比按分鐘/秒/毫秒來的好)。另外對于索引存在實(shí)時(shí)寫入(段文件頻繁被refresh的場景),以及其他會(huì)導(dǎo)致緩存實(shí)效、穿透的場景,緩存結(jié)果不明顯。
  • 如果一個(gè)terms查詢會(huì)命中大量的文檔(不考慮其他篩選條件,只考慮terms查詢本身,比如會(huì)命中索引中90%的文檔,或上千萬文檔),強(qiáng)烈建議手動(dòng)轉(zhuǎn)化為boolean-should查詢并放置于boolean filter中或者分多次查詢,每次查詢保持terms在16個(gè)以下。否則可能會(huì)導(dǎo)致查詢占用大量資源以至于集群不可用。但也不能一味地轉(zhuǎn)換為boolean query,否則可能會(huì)超過boolean query的子語句上限(1024)導(dǎo)致查詢報(bào)錯(cuò),也可能會(huì)導(dǎo)致占用更多的內(nèi)存,影響查詢性能。
  • 合理設(shè)置超時(shí)時(shí)間,對于確保集群的穩(wěn)定性至關(guān)重要
  • 業(yè)務(wù)上游服務(wù)應(yīng)具備讀寫限流能力,避免業(yè)務(wù)應(yīng)讀寫流量過大把集群打掛(極端情況下,譬如AZ級故障需摘除一個(gè)機(jī)房機(jī)器,集群查詢能力減半,此時(shí)如業(yè)務(wù)查詢負(fù)載過高,必須進(jìn)行限流,避免集群被打掛)
  • ES為近實(shí)時(shí)搜索引擎,查詢結(jié)果最終一致,這就意味著可能存在寫入后無法立即查詢命中的狀況,查詢實(shí)時(shí)性越高,對集群的寫負(fù)載壓力越高。ES不支持事務(wù),對于存在大量更新、刪除場景的業(yè)務(wù)場景,非常容易出現(xiàn)版本沖突問題,業(yè)務(wù)如事務(wù)有非常高的要求的話,需要進(jìn)行CAS版本控制,可以用ES原生的版本控制實(shí)現(xiàn)(ES5:文檔version控制;ES7:_seq_no & _primary_term控制,加強(qiáng)了ES節(jié)點(diǎn)故障后CAS的異常處理能力),也可以使用external外部版本控制(業(yè)務(wù)自行設(shè)計(jì)字段控制文檔版本)。
4.3 容災(zāi)架構(gòu)
  • 索引配置至少1份副本,確保數(shù)據(jù)冗余
  • 查詢?nèi)萘砍渥?,核心S級集群機(jī)器CPU利用率最大值低于50%
  • 磁盤容量充足,機(jī)器磁盤空間使用率<80%
  • 當(dāng)集群出現(xiàn)AZ故障,掛掉一個(gè)機(jī)房時(shí),確保另一個(gè)機(jī)房擁有足夠查詢?nèi)萘浚軌蚶^續(xù)提供服務(wù)。建議業(yè)務(wù)平時(shí)執(zhí)行AZ逃生演練,確保集群摘除一半節(jié)點(diǎn)后仍能正常提供服務(wù)
  • 多集群部署,單集群出現(xiàn)故障時(shí),可快速切流至備用集群,降低業(yè)務(wù)影響時(shí)長
  • 業(yè)務(wù)寫數(shù)據(jù)時(shí),當(dāng)集群出現(xiàn)故障時(shí),業(yè)務(wù)需要具備重試寫入能力,以確保數(shù)據(jù)完整(至少需要具備數(shù)據(jù)補(bǔ)錄能力)。重要數(shù)據(jù)建議寫MQ隊(duì)列(如Kafka),持久化數(shù)據(jù),或使用數(shù)據(jù)比對服務(wù)(BCP),來確保數(shù)據(jù)一致性。同時(shí)寫入數(shù)據(jù)具備冪等[1]能力(即寫入文檔時(shí)指定id),避免數(shù)據(jù)重復(fù)問題。
5、ES分頁查詢

參考官方文檔:Paginate search results

5.1 From+Size
  • 查詢示例
    from:未指定,默認(rèn)值是 0,注意不是1,代表當(dāng)前頁返回?cái)?shù)據(jù)的起始值。
    size:未指定,默認(rèn)值是 10,代表當(dāng)前頁返回?cái)?shù)據(jù)的條數(shù)。
GET kibana_sample_data_flights/_search 
{ "from": 0, "size":20, "query": { "match": { "DestWeather": "Sunny" } }, "sort": [ { "FlightTimeHour": { "order": "desc" } } ] }
  • 優(yōu)缺點(diǎn)及適用場景
    優(yōu)點(diǎn):支持隨機(jī)翻頁。
    缺點(diǎn):受制于 index.max_result_window 設(shè)置,不能無限制翻頁。無法應(yīng)用于深翻頁。越往后翻頁越慢,深度過深會(huì)消耗大量資源。
    適用場景:第一:非常適合小型數(shù)據(jù)集或者大數(shù)據(jù)集返回 Top N(N <= 10000)結(jié)果集的業(yè)務(wù)場景。第二:類似主流 PC 搜索引擎(谷歌、bing、百度、360、sogou等)支持隨機(jī)跳轉(zhuǎn)分頁的業(yè)務(wù)場景。
  • 為何不支持深度分頁
GET kibana_sample_data_flights/_search 
{ "from": 0, "size":10001 } GET kibana_sample_data_flights/_search { "from": 10001, "size":10 }
{ "error" : { "root_cause" : [ { "type" : "illegal_argument_exception", "reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting." } ],
  • 搜索請求通??缭蕉鄠€(gè)分片,每個(gè)分片必須將其請求的命中內(nèi)容以及任何先前頁面的命中內(nèi)容加載到內(nèi)存中。
  • 對于翻頁較深的頁面或大量結(jié)果,這些操作會(huì)顯著增加內(nèi)存和 CPU 使用率,從而導(dǎo)致性能下降或節(jié)點(diǎn)故障。
5.2 Search after+PIT

Search after是ES官方提出的解決深分頁問題的標(biāo)準(zhǔn)方案,這種翻頁方式的翻頁深度沒有限制。

  • Search after

    • search after需要用戶在查詢請求中指定排序字段(需要是唯一ID類型的字段,或者是多個(gè)字段的組合是唯一的),然后在下次請求中設(shè)置search after參數(shù)值為上次查詢結(jié)果中最后一個(gè)結(jié)果的排序值,即可實(shí)現(xiàn)翻頁。
    • search_after 查詢本質(zhì):使用前一頁中的一組排序值來檢索匹配的下一頁。
    • 但是因?yàn)閟earch after默認(rèn)是實(shí)時(shí)查詢當(dāng)前數(shù)據(jù),如果在深分頁查詢過程中,數(shù)據(jù)有更新,則可能導(dǎo)致分頁結(jié)果不符合預(yù)期。所以如果要保證在整個(gè)search after深分頁查詢過程中有一致的查詢結(jié)果,那么我們就要使用pit來保留查詢開始時(shí)索引的狀態(tài)。
  • PIT
    在大部分翻頁場景下,我們要求后續(xù)的多個(gè)請求返回與第一次查詢是相同的排序結(jié)果序列。也就是說,即便在后續(xù)翻頁的過程中,可能會(huì)有新數(shù)據(jù)寫入等操作,但這些操作不會(huì)對原有結(jié)果集構(gòu)成影響。如何使用search after實(shí)現(xiàn)呢?可以創(chuàng)建一個(gè)時(shí)間點(diǎn) Point In Time(PIT)保障搜索過程中保留特定事件點(diǎn)的索引狀態(tài)。Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。PIT的本質(zhì):存儲(chǔ)索引數(shù)據(jù)狀態(tài)的輕量級視圖。
    如下示例能很好的解讀 PIT 視圖的內(nèi)涵。

# 創(chuàng)建 PIT
POST kibana_sample_data_logs/_pit?keep_alive=1m

# 獲取數(shù)據(jù)量 14074
POST kibana_sample_data_logs/_count

# 新增一條數(shù)據(jù)
POST kibana_sample_data_logs/_doc/14075
{
  "test":"just testing"
}

# 數(shù)據(jù)總量為 14075
POST kibana_sample_data_logs/_count


# 查詢PIT,數(shù)據(jù)依然是14074,說明走的是之前時(shí)間點(diǎn)的視圖的統(tǒng)計(jì)。
POST /_search
{
  "track_total_hits": true, 
  "query": {
    "match_all": {}
  }, 
   "pit": {
    "id": "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEN3RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA"
  }
}

有了 PIT,search_after 的后續(xù)查詢都是基于 PIT 視圖進(jìn)行,能有效保障數(shù)據(jù)的一致性。

  • 使用方式
    這里我們介紹search after+pit的使用方式,如果有業(yè)務(wù)不需要pit,那么自行去除pit相關(guān)操作即可。search_after 分頁查詢可以簡單概括為如下幾個(gè)步驟。
    步驟 1:創(chuàng)建 PIT 視圖。
# Step 1: 創(chuàng)建 PIT 
POST kibana_sample_data_logs/_pit?keep_alive=1m

返回結(jié)果如下:

{ "id" : "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA" }

keep_alive=1m,類似scroll的參數(shù),代表視圖保留時(shí)間是 1 分鐘,超過 1分鐘執(zhí)行會(huì)報(bào)錯(cuò)如下:

"type" : "search_context_missing_exception", "reason" : "No search context found for id [91600]"

步驟 2:創(chuàng)建基礎(chǔ)查詢語句,這里要設(shè)置翻頁的條件。
其中排序字段需要是不同文檔有唯一值的字段,或者是使用多個(gè)字段排序而多個(gè)字段的組合是唯一的。

  • 設(shè)置了PIT,檢索時(shí)候就不需要再指定索引。
  • pit的id 是基于步驟1 返回的 id 值。
  • 排序 sort 指的是:按照哪個(gè)關(guān)鍵字排序。
# Step 2: 創(chuàng)建基礎(chǔ)查詢
GET /_search
{
  "size":10,
  "query": {
    "match" : {
      "host" : "elastic"
    }
  },
  "pit": {
     "id":  "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA", 
     "keep_alive": "1m"
  },
  "sort": [ 
    {"productId": "asc"}
  ]
}

返回結(jié)果中的排序結(jié)果在ES7和ES8有不同的表現(xiàn):
ES7版本:會(huì)返回一個(gè)值也就是你的排序值。

"sort" : [ "200" ]

ES8版本以上:除了業(yè)務(wù)自己的排序值以外,還會(huì)多出來一個(gè)值,如下所示:

"sort" : [ "200", 4 ]

其中,“200”就是我們指定的排序方式:基于 {"productId": "asc"} 升序排列。
而 4 代表什么含義呢??4 代表——隱含的排序值,是基于_shard_doc 的升序排序方式。額外的_shard_doc字段的功能也就是tiebreaker。ES使用這種方式來解決用戶提供的排序字段值不唯一可能會(huì)導(dǎo)致分頁結(jié)果異常的問題。tiebreaker 本質(zhì)含義:每個(gè)文檔的唯一值,確保分頁不會(huì)丟失或者分頁結(jié)果數(shù)據(jù)出現(xiàn)重復(fù)(相同頁重復(fù)或跨頁重復(fù))。

步驟3:實(shí)現(xiàn)后續(xù)翻頁。
其中 search_after 的值是前一頁的最后一個(gè)文檔的 sort 字段值,在本例子中為:"search_after" : [ "200" ]或"search_after": [ "200", 4 ]pit的值需要指定為上次查詢翻頁請求的返回結(jié)果中的pit值??梢栽O(shè)置track_total_hits為false,避免無用的計(jì)數(shù),加快查詢。query和sort需要和之前的查詢保持一致。

# step 3 : 開始翻頁
GET /_search
{
  "size": 10,
  "query": {
    "match" : {
      "host" : "elastic"
    }
  },
  "pit": {
     "id":  "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA", 
     "keep_alive": "1m"
  },
  "sort": [
    {"response.keyword": "asc"}
  ],
  "search_after": [                                
    "200",
    4
  ],
  "track_total_hits": false      
}

顯然,search_after 查詢僅支持向后翻頁。

步驟4:清理pit在翻頁完成之后需要及時(shí)清理pit釋放資源。

DELETE /_pit
{
    "id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
  • search_after 查詢優(yōu)缺點(diǎn)及適用場景
    search_after 優(yōu)點(diǎn):不嚴(yán)格受制于 max_result_window,可以無限制往后翻頁。ps:不嚴(yán)格含義:單次請求值不能超過 max_result_window;但總翻頁結(jié)果集可以超過。
    search_after 缺點(diǎn):只支持向后翻頁,不支持隨機(jī)翻頁。
    search_after 適用場景,類似:今日頭條分頁搜索 https://m.toutiao.com/search;來自于用戶的深分頁請求不支持隨機(jī)翻頁,更適合手機(jī)端應(yīng)用的場景。
5.3 Scroll查詢
  • 使用方式

步驟 1:指定檢索語句同時(shí)設(shè)置 scroll 上下文保留時(shí)間。
實(shí)際上,scroll 已默認(rèn)包含了 search_after 的PIT 的視圖或快照功能。
從 Scroll 請求返回的結(jié)果反映了發(fā)出初始搜索請求時(shí)索引的狀態(tài),類似在那一個(gè)時(shí)刻做了快照。隨后對文檔的更改(寫入、更新或刪除)只會(huì)影響以后的搜索請求。
游標(biāo)的存活時(shí)間為保留scroll context到下次請求的時(shí)間,每次翻頁請求都會(huì)重新計(jì)時(shí)。所以不需要設(shè)置存活時(shí)間太長,只需要能保證覆蓋每次翻頁請求的間隔時(shí)間即可。

POST kibana_sample_data_logs/_search?scroll=3m 
{ "size": 100, "query": { "match": { "host": "elastic" } } }

步驟 2:向后翻頁繼續(xù)獲取數(shù)據(jù),直到?jīng)]有要返回的結(jié)果為止。

POST _search/scroll 
{ "scroll" : "3m", "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFkY4UkIwZWtlU2d1OTdTUjRIbzVXdHcAAAAAAAGmkBZ0bVM5YUxMX1R1Nkd1VkNiaGhZSWNn" }

scroll_id 值是步驟 1 返回的結(jié)果值。
步驟 3:清理scroll context
在查詢完成之后,需要及時(shí)清理scroll context,以釋放資源。

DELETE /_search/scroll
{
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

java樣例代碼

package com.sankuai.meituan.eagle.example;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;

import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ScrollExample {

    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    @Autowired
    private RestHighLevelClient porosRestHighLevelClient;

    public void execQuery() throws IOException {
        // 構(gòu)造查詢請求
        final BoolQueryBuilder query = QueryBuilders.boolQuery()
                .must(QueryBuilders.termQuery("OriginCountry", "CN"));
        final SearchSourceBuilder builder = new SearchSourceBuilder().query(query)
                .sort("_doc") // 如果沒有特殊的排序要求,這里設(shè)置_doc排序,性能更好。
                .size(100);
        final SearchRequest req =
                new SearchRequest(new String[]{"kibana_sample_data_flights"}, builder);
        // 開啟游標(biāo), 并設(shè)置存活時(shí)間
        req.scroll("1m");

        // 提交查詢獲取游標(biāo)
        SearchResponse res = porosRestHighLevelClient.search(req, RequestOptions.DEFAULT);
        // 處理結(jié)果集
        processDocs(res.getHits().getHits());
        // 滾動(dòng)游標(biāo)處理結(jié)果集, 直到?jīng)]有獲取到數(shù)據(jù)。
        while (res.getHits().getHits().length > 0) {
            res = porosRestHighLevelClient.searchScroll(new SearchScrollRequest(res.getScrollId()).scroll("1m"));
            processDocs(res.getHits().getHits());
        }
      
        // 查詢完成之后主動(dòng)清理scroll context
        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(res.getScrollId());
        porosRestHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);      
    }

    private static void processDocs(SearchHit[] hits) {
        Arrays.stream(hits).forEach(it -> log.info("next: {}", it.getSourceAsString()));
    }
}
  • 及時(shí)清理scroll context
    需要注意的是,游標(biāo)是有一定資源代價(jià)的,建議在使用完畢后手動(dòng)刪除釋放游標(biāo)。并且控制游標(biāo)的存活時(shí)間參數(shù)在合理值。
    因?yàn)閟croll context會(huì)占用大量資源,所以ES會(huì)限制集群中當(dāng)前打開的scroll context個(gè)數(shù)。默認(rèn)為500。通過配置項(xiàng)search.max_open_scroll_context控制。
    如果遇到報(bào)錯(cuò)scroll context過多,強(qiáng)烈不建議調(diào)整配置項(xiàng)search.max_open_scroll_context的值。建議檢查是否在查詢完成之后主動(dòng)清理了search context,并且減少scroll的存活時(shí)間?;蛘呖梢钥紤]改用search after+pi

  • scroll 查詢的原理:

    • ES的搜索是分2個(gè)階段進(jìn)行的,即Query階段和Fetch階段。 Query階段分發(fā)查詢到每個(gè)分片,每個(gè)分片通過查詢倒排索引,獲取滿足查詢結(jié)果的文檔ID列表。將每個(gè)shard的結(jié)果取回之后,在協(xié)調(diào)結(jié)點(diǎn)進(jìn)行全局排序。 通過from+size這種方式分頁獲取數(shù)據(jù)的時(shí)候,隨著from加大,需要全局排序并丟棄的結(jié)果數(shù)量隨之上升,性能越來越差。
    • 而Scroll查詢,先做輕量級的Query階段以后,免去了繁重的全局排序過程。 它只是將查詢結(jié)果集,也就是doc_id列表保留在一個(gè)上下文里, 之后每次分批取回的時(shí)候,只需根據(jù)設(shè)置的size,在每個(gè)shard內(nèi)部按照一定順序(默認(rèn)doc_id序), 取回這個(gè)size數(shù)量的文檔即可。
      而存儲(chǔ)這個(gè)上下文畢竟還是有一定的資源代價(jià)的,這也是我們需要注意游標(biāo)的過期時(shí)間以及必要時(shí)采取手動(dòng)清理的原因。
    • 綜上我們可以很容易推斷出Scroll并不適用于實(shí)時(shí)分頁查詢的場景,它的主要用途是從ES集群拉取大量結(jié)果集進(jìn)行離線處理的應(yīng)用場景。比如,需要掃出滿足條件的所有記錄做ETL轉(zhuǎn)存或者導(dǎo)出之類。
  • scroll查詢優(yōu)缺點(diǎn)及適用場景:

    • scroll 查詢優(yōu)點(diǎn):支持全量遍歷。ps:單次遍歷的 size 值也不能超過 index.max_result_window 大小。
    • scroll 查詢?nèi)秉c(diǎn):響應(yīng)時(shí)間非實(shí)時(shí)。?不適用于來自于用戶的深分頁請求;保留上下文需要足夠的堆內(nèi)存空間。
    • scroll 查詢適用場景:全量或數(shù)據(jù)量很大時(shí)遍歷結(jié)果數(shù)據(jù),而非分頁查詢。官方文檔強(qiáng)調(diào):不再建議使用scroll API進(jìn)行深度分頁。如果要分頁檢索超過 Top 10,000+ 結(jié)果時(shí),推薦使用:PIT + search_after。
5.4 總結(jié)
  • From+ size:需要隨機(jī)跳轉(zhuǎn)不同分頁(類似主流搜索引擎)、Top 10000 條數(shù)據(jù)之內(nèi)分頁顯示場景。
  • search_after:僅需要向后翻頁的場景及超過Top 10000 數(shù)據(jù)需要深分頁場景。
  • Scroll:需要離線遍歷全量數(shù)據(jù)場景 。
  • index.max_result_window:調(diào)大治標(biāo)不治本,不建議調(diào)過大。
  • PIT:本質(zhì)是視圖。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容