ElasticSearch&Lucene學習總結

https://zhuanlan.zhihu.com/Elasticsearch
阿里雲TableStore團隊出品的ElasticSearch技術研討學習總結

ES定位

搜索領域:基於lucene
Json文檔數據庫: 相對於MongoDB讀寫性能更佳
時序數據分析:如日誌處理,監控數據存儲,分析和數據化

基本概念:

節點(Node): 一個ES實例

索引(Index): 邏輯概念,配置信息mapping和倒排索引,正排索引文件。索引可以分佈在一臺或者多臺機器

分片(Shard): 爲了支持更大量的數據,索引一般會按照某個維度分成多個部分。爲了可靠性和可用性,同一個索引的分片會盡量分佈在不同節點上以保證高可用。分片有兩種分別是主分片和副本分片

副本: 同一個分片的備份數據,一個分片可能會有0或者多個副本。

在這裏插入圖片描述

  • 建立索引流程

doc先經過路由規則定位到主Shard,發送這個doc到主Shard建索引,成功之後再發送這個doc到Shard的副本上建立索引,等到副本上建立索引成功後才返回成功。

這時候,如果某個副本Shard或者主Shard丟失的時候,需要從其他副本全量拷貝到這個節點上構造新的分片。

副本(Replica)存在的一個理由,避免數據丟失,提高數據可靠性。副本(Replica)存在的另一個理由是讀請求量很大的時候,一個Node無法承載所有流量,這個時候就需要一個副本來分流查詢壓力,目的就是擴展查詢能力。

部署方式

1.混合部署
如下面左圖:

  • 如果不考慮主節點,還會有Data Node和Transport Node,這種部署模式下,這兩種不同類型Node角色都會位於同一個Node中。

  • 當有query請求到達的時候,會把請求隨機發送給任何一個node,這個node會有一個全局的路由表,然後再通過路由表選擇合適的Node,將請求發送給這些Node.然後等所有請求都返回之後,把結果合併返回給用戶。

  • 缺點:

多種類型的請求會相互影響,如果某個節點出現熱點,那麼會影響經過這個節點的所有跨Node請求。

在這裏插入圖片描述

2.分層部署

  • 通過配置來隔離開節點的功能。
  • 設置部分節點爲傳輸結點,專門用來做請求轉發與結果合併
  • 其他節點爲數據節點,專門用來處理數據
  • 好處是角色相互獨立,不會相互影響。一般Transport Node流量平均分配,很少會出現單臺機器CPU或者流量被打滿的情況。
  • 支持熱更新: 指的是可以先一臺臺地升級DataNode,升級完成之後再升級Transport Node,整個過程可以做到用戶無感知。
  • 角色獨立後,只需要Transport Node連接所有的DataNode,而DataNode則不需要和其他DataNode有連接。一個集羣中DataNode的數量遠大於Transport Node,這樣集羣的規模可以更大。

Es數據層架構

數據存儲

Es的Index數據和meta存儲在本地文件系統,這帶來的問題就是如果當一臺機器宕機或者磁盤損壞的時候,數據就會丟失。

可以使用副本機制來解決這個問題。

副本

1.服務可用性: 當設置多個副本的時候,如果某一個副本不可用,那麼請求可能會流向其他的副本,服務很快就可以恢復

2.保證數據可靠: 如果沒有副本機制,當主分片的存在磁盤的數據丟失的時候,這個節點的所有分片數據都會丟失。

3.提供更強大的查詢能力: 當分片提供的查詢能力無法滿足業務需求的時候,可以把流量負載到副本上提高查詢的併發度。

基於本地文件系統的分佈式系統

這種架構的特點就是存儲和計算是合併起來的。

在這裏插入圖片描述

每個節點的本地文件系統保存着分片數據,當一個節點發生宕機的時候,發生主備切換,然後需要再找一臺機器當作備份,這就涉及到分片數據從一臺機器的本地磁盤傳輸到另外一臺機器的本地磁盤。在這個期間分片不能提供服務。

此外這種架構還有其他缺點:

1.多副本機制帶來成本浪費。當作副本的Shard的計算能力往往會被浪費。

2.寫性能和吞吐量的下降,每次索引或者更新的時候,需要先去更新Primary Replica,然後異步更新其他從副本來保證數據的一致性,寫入性能會下降。

3.當出現熱點或者需要緊急擴容的時候動態增加Replica慢。新Shard的數據需要完全從其他Shard拷貝,拷貝時間較長。

ElasticSearch,Kafka就是採用這種架構。

基於分佈式文件系統的分佈式系統(共享存儲)

這種架構的特點就是存儲和計算是分離的。
在這裏插入圖片描述

採取存儲和計算分離的思想,上一種思路的問題主要在跨節點拷貝數據十分耗時。

一種解決方法的思路就是使用分佈式文件系統作爲底層的存儲。每一個分片只需要連接到一個分佈式文件系統的目錄或者文件,分片本身不含有數據。

相當於節點存放着指向真實數據的指針,只負責計算不負責存儲。

擴容的時候,只需要新節點的分片指向分佈式文件系統即可。

但可能訪問分佈式文件系統的性能會比不過本地文件系統。

Hbase採用這種架構方式。

Lucene數據模型

  • Index: 索引,由很多的Document組成,相當於關係型數據庫的Database

  • Document: 由很多的Field組成,是Index和Search的最小單位 相當於關係型數據庫的Row

  • Field: 由很多的Term組成,包括Field Name和Field Value

  • Term 由很多的字節組成,可以分詞

ElasticSearch擁有全部上面四種數據類型

Lucene中存儲的索引主要分爲三種類型:

1.Invert Index:倒排索引

可以配置爲是否分詞,如果分詞的話可以設置不同的分詞器。

DOCS:只存儲DocID。
DOCS_AND_FREQS:存儲DocID和詞頻(Term Freq)。
DOCS_AND_FREQS_AND_POSITIONS:存儲DocID、詞頻(Term Freq)和位置。
DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS:存儲DocID、詞頻(Term Freq)、位置和偏移

根據字段查詢包含該字段的文檔ID以及詞頻,位置。

Key: Term
Value: DocID 的鏈表

2.DocValues: 正排索引

採用列式存儲,通過DocID可以快速讀取該文檔所有的term

Key: DocID和Filed Name
Value:Field Value

3.Store:字段原始內容存儲

同一篇文章的多個Field的Store會存儲在一起,使用場景:一次讀取少量且多個字段內存的場景。

Key: DocID
Value: Field Key和Field Value

Lucene中提供索引和搜索的最小組織形式是Segment,Segment按照索引類型不同,會分爲上面介紹的三種,每一類都是按照文檔爲最小單位存儲。

Lucene的不足

1.Lucene是一個單機的搜索庫,不支持分佈式搜索
2.沒有更新操作,每次都是Append一個新文檔,那如何做部分字段的更新?
3.Lucene沒有主鍵索引,如何處理同一個Doc的多次寫入
4.在稀疏列數據中,如何判斷某些文檔是否存在特定字段
5.Lucene生成完整的Segment之後,這個Segment不能再被更改,這個時候Segment才能被搜索。那如何做實時搜索?

ElasticSearch 如何支持分佈式?

增加了一個系統字段 ”_routing“(路由)
通過這個字段把文檔分發到不同的Shard,那麼不同的Shard可以位於不同的機器上,這樣就能實現簡單的分佈式。

下面是Es相對於Lucene增加了一些新的字段。

1._id

Lucene中沒有主鍵索引,要保證系統中同一個Doc不會重複,Elasticsearch引入了_id字段來實現主鍵。每次寫入的時候都會先查詢id,如果有,則說明已經有相同Doc存在了。

解決了Lucene沒有主鍵索引,如何處理同一個Doc的多次寫入

2._version

Es中每個文檔都會有版本號,可以通過doc_id來讀取Version。所以Version只要存儲爲DocValues即可。類似於KeyValue存儲。

Es使用version,一種樂觀鎖的思想來保證文檔的變更以正確的順序執行。

1.首次寫入文檔的時候,爲文檔分配一個初始的版本號

2.再次寫入文檔,分兩種情況:

  • 請求沒有指定Version

先加鎖,然後去讀取該Doc最大版本V1,然後把V1+1新版本寫入Lucene

  • 請求指定了Version

先加鎖,然後讀取最大版本V2,只有當指定版本V1==V2的時候再寫入,否則發生版本衝突

3.部分更新的時候,會先通過GetRequest讀取當前id的完整Doc和V1,接着和當前Request中的Doc合併爲一個完整Doc。
再執行一些邏輯後加鎖,查看最大版本號是否等於請求時候指定的版本號,假如不是,說明其他線程更改了這個文檔,需要報錯之後重試。如果相等,說明沒有其他線程修改當前文檔,則可以繼續寫入lucene.

解決Lucene問題:“沒有更新操作,每次都是Append一個新文檔,那如何做部分字段的更新?

3… _routing

路由規則,寫入和查詢的routing是需要一致的。

引入_routing字段的作用:

1.查詢使用某種_routing的文檔有哪些,當路由規則發生變化的時候,可以根據routing讀取文檔對其reindex,存儲爲倒排索引

2.查詢到文檔後,要在Response裏面展示文檔使用的路由規則,存儲爲Store

解決Lucene問題: 支持分佈式搜索引擎

4._field_names

用來在稀疏文檔中查詢那些文檔存在Field
因此這裏需要倒排索引Index

解決Lucene問題:在稀疏列數據中,如何判斷某些文檔是否存在特定字段
5. _source

存儲原始文檔。這個字段的功能在於通過doc_id讀取這個文檔的原始內容,所以只需要存儲Store。

Elasticsearch中使用_source字段可以實現以下功能:

Update:部分更新時,需要從讀取文檔保存在_source字段中的原文,然後和請求中的部分字段合併爲一個完整文檔。如果沒有_source,則不能完成部分字段的Update操作。

Rebuild:最新的版本中新增了rebuild接口,可以通過Rebuild API完成索引重建,過程中不需要從其他系統導入全量數據,而是從當前文檔的_source中讀取。如果沒有_source,則不能使用Rebuild API。

Script:不管是Index還是Search的Script,都可能用到存儲在Store中的原始內容,如果禁用了_source,則這部分功能不再可用。

Summary:摘要信息也是來源於_source字段。

6._seq_no

每個文檔有一個單調遞增的序號,序列號的一個作用是可以用來實現checkpoint.

每個文檔在使用Lucene的document操作接口之前,會得到一個seq_no,然後文檔寫入Lucene之後,會用這個seq_no來更新本地checkpoint

local_checkpoint和global_checkpoint,主要用於保存有序性,另一方面減少數據拷貝時候的數據拷貝量。

引入該字段主要作用:

一是通過doc_id查詢到該文檔的seq_no,

二是通過seq_no範圍查找相關文檔,所以也就需要存儲爲Index和DocValues(或者Store)。

由於是在衝突檢測時才需要讀取文檔的_seq_no,而且此時只需要讀取_seq_no,不需要其他字段,這時候存儲爲列式存儲的DocValues比Store在性能上更好一些。

7.primary_term
_primary_term也和_seq_no一樣是一個整數,每當Primary Shard發生重新分配時,比如重啓,Primary選舉等,_primary_term會遞增1。

_primary_term主要是用來恢復數據時處理當多個文檔的_seq_no一樣時的衝突,避免Primary Shard上的寫入被覆蓋。

內核解析——寫入

ES有兩個身份: 一個是分佈式搜索系統,另外一個是分佈式NoSQL數據庫。

目前的ES有兩種身份:一是分佈式搜索引擎,二是分佈式NoSQL。

寫操作:

  • 實時
    1.搜索系統的Index一般是近實時的,Index的實時性是由refresh控制的,默認是1s,也就是說文檔寫入後進行索引後,需要等待至少1s才能被搜索到。

2.NoSQL數據庫的寫操作基本是實時的。寫入成功後,立即是可見的。Es中的Index請求也能保證是實時的,因爲Get請求會直接讀內存中尚未Flush到外存上的TransLog

  • 可靠

1.搜索系統的可靠性要求不高,般數據的可靠性通過將原始數據存儲在另一個存儲系統來保證,當搜索系統的數據發生丟失時,再從其他存儲系統導一份數據過來重新rebuild就可以了。在Es中,可以通過設置TransLog的Flush頻率來保證可靠性。一般Flush的時間間隔越長,可靠性會越低。

2.NoSQL數據庫如果作爲一款數據庫,需要保證數據的強可靠性。TransLog的Flush策略需要爲每一個請求進行flush。當前Shard寫入成功後,數據能儘量持久化下來。

什麼是refresh和flush操作?

當我們向ES發送請求的時候,我們發現es貌似可以在我們發請求的同時進行搜索。而這個實時建索引並可以被搜索的過程實際上是一次es 索引提交(commit)的過程,如果這個提交的過程直接將數據寫入磁盤(fsync)必然會影響性能,所以es中設計了一種機制,即:先將index-buffer中文檔(document)解析完成的segment寫到filesystem cache之中,這樣避免了比較損耗性能io操作,又可以使document可以被搜索。以上從index-buffer中取數據到filesystem cache中的過程叫做refresh

而flush操作指的是,把文件系統緩存中的數據刷寫到磁盤,同時清空對應操作的translog。因爲filesystem cache中的數據很有可能會隨着es進程宕機而丟失,所以需要有一種機制能夠保證filesystem中的數據不會丟失,重啓的時候就可以把這個記錄將數據恢復過來。

在這裏插入圖片描述

translog的功能:
1.保證在filesystem cache中的數據不會因爲elasticsearch重啓或是發生意外故障的時候丟失。
2.當系統重啓時會從translog中恢復之前記錄的操作。
3.當對elasticsearch進行CRUD操作的時候,會先到translog之中進行查找,因爲tranlog之中保存的是最新的數據。
4.translog的清除時間時進行flush操作之後(將數據從filesystem cache刷入disk之中)。

每次 index、bulk、delete、update 完成的時候,一定觸發flush到磁盤上,纔給請求返回 200 OK。這個改變提高了數據安全性,但是會對寫入的性能造成影響。

Lucene的寫機制

Es中的Lucene可以完成簡單的索引創建和搜索功能,但是沒有解決以下問題:

1.Lucene只支持單機,而不是分佈式
2.文檔寫入Lucene之後不是立刻可以查詢的,需要生成完整的Segment之後才能被搜索
3.Lucene生成的Segment是在內存中,如果機器宕機或者掉電之後,內存的Segment會丟失,那麼如何保證數據可靠性?
4.Lucene不支持部分文檔更新

所以以上問題只能由ElasticSearch來解決:

Es採用多Shard的方式,通過配置routing規則把數據分成多個數據子集,每一個數據子集都會提供獨立的索引和搜索功能。寫入文檔時候,會根據routing規則再把文檔發送給特定的Shard建立索引。

Es架構採用了一主多副的方式:
在這裏插入圖片描述

每一個Index由多個Shard組成,每一個Shard有一個主節點或者多個副本節點,而且副本節點的數目是可以配置的。但是每次寫入的時候,寫入請求會先根據_routing規則選擇發送給哪一個分片。

Index Request中可以設置使用哪一個Field的值作爲路由參數。如果沒有設置,則使用Mapping中的配置,如果mapping中也沒有配置,就使用_id作爲路由參數。最後再通過_routing的Hash值選擇出Shard分片,最後從集羣的Meta中找出這個Shard的Primary節點。

請求會接着發送給Primary Shard,在Primary Shard執行成功之後會把請求同時發送給多個Replica Shard,請求會在多個Replica Shard之後,寫入請求執行成功就會把結果返回給客戶端。

寫入操作的延時就等於latency = Latency(Primary Write) + Max(Replicas Write)。只要有副本在,寫入延時最小也是兩次單Shard的寫入時延總和,寫入效率會較低。但是這樣能夠保證數據的可靠性。

採用多個副本之後,可以避免單機或者磁盤故障發生。但是如果頻繁進行磁盤IO,會對讀寫性能造成一定影響。默認是每五分鐘纔會把Lucene的Segment寫入磁盤持久化,對於寫入內存,但是還沒有Flush到磁盤的Lucene數據。如果發現機器宕機或者掉電,那麼內存中的數據將會丟失對吧。

Es採用的方法是增加一個CommitLog模塊,Es中叫做TransLog

在這裏插入圖片描述

下面總結一下Es的寫入流程:

寫入請求到分片之後,會先寫Lucene文件,然後創建好索引。這個時候索引還會在內存裏面,然後纔會再去寫TransLog,寫完TransLog之後,會flush到磁盤。寫磁盤成功之後,才把請求返回給用戶。

關鍵點分析:

1.數據庫會先寫CommitLog,然後再寫內存,而Es是先寫內存,最後才寫TransLog。一種可能的原因是Lucene的內存寫入會有很複雜的邏輯,很容易失敗,比如分詞,字段長度超過限制等,比較重,爲了避免TransLog中有大量無效記錄,減少recover的複雜度和提高速度,所以就把寫Lucene放在了最前面。

2.二是寫Lucene內存後(Index Buffer),文檔是暫時不能被搜索到的。需要通過Refresh把在Index Buffer中的內存對象轉換成完整的Segment,保存在文件系統內存中,這個時候才能被搜索到。這個時間一般設置爲1秒鐘。這也是爲什麼Es在搜索其實是近實時的。

3.默認每隔30min,Lucene會把文件系統內存中的Segment刷到磁盤中,刷寫之後索引文件已經持久化了,歷史的TransLog就會沒用,會清空掉舊的TransLog。

4.如果Es作爲NoSQL數據庫使用的時候,查詢方式是GetById,這種查詢直接可以從TransLog查詢,這就變成了實時的查詢系統。

下面總結一下Es的更新流程:

在這裏插入圖片描述

收到Update請求後,從Segment或者TransLog中讀取同id的完整Doc,記錄版本號爲V1。

將版本V1的全量Doc和請求中的部分字段Doc合併爲一個完整的Doc,同時更新內存中的VersionMap。獲取到完整Doc後,Update請求就變成了Index請求。

加鎖。

再次從versionMap中讀取該id的最大版本號V2,如果versionMap中沒有,則從Segment或者TransLog中讀取,這裏基本都會從versionMap中獲取到。

檢查版本是否衝突(V1==V2),如果衝突,則回退到開始的“Update doc”階段,重新執行。如果不衝突,則執行最新的Add請求。

在Index Doc階段,首先將Version + 1得到V3,再將Doc加入到Lucene中去,Lucene中會先刪同id下的已存在doc id,然後再增加新Doc。寫入Lucene成功後,將當前V3更新到versionMap中。

釋放鎖,部分更新的流程就結束了。

Es對應分佈式系統的六大特性:

1.可靠性

通過副本機制和Translog保持

2.一致性

Lucene中的Flush鎖只會保證Update接口裏的Delete和Add中間不會Flush,

3.原子性:

Add和Delete都是直接調用Lucene的接口,是原子的。當部分更新時,使用Version和鎖保證更新是原子的。

4.隔離性:

仍然採用Version和局部鎖來保證更新的是特定版本的數據。

5.實時性:

使用定期Refresh Segment到內存,並且Reopen Segment方式保證搜索可以在較短時間(比如1秒)內被搜索到。通過將未刷新到磁盤數據記入TransLog,保證對未提交數據可以通過ID實時訪問到。

一是不需要所有Replica都返回後才能返回給用戶,只需要返回特定數目的就行;

二是生成的Segment現在內存中提供服務,等一段時間後才刷新到磁盤,Segment在內存這段時間的可靠性由TransLog保證;

三是TransLog可以配置爲週期性的Flush,但這個會給可靠性帶來傷害;

四是每個線程持有一個Segment,多線程時相互不影響,相互獨立,性能更好;

五是系統的寫入流程對版本依賴較重,讀取頻率較高,因此採用了versionMap,減少熱點數據的多次磁盤IO開銷。

查詢篇

同樣,Es的查詢對於搜索引擎來說是近實時的,而對於NoSQL來說是實時的。

一致性指的是寫入成功之後,下次讀的操作一定要讀取到最新的數據。對於搜索而言對於強一致性的要求會低一點,但是對於NoSQL來說,最好是強一致性的。

搜索系統一般是兩階段查詢,第一個階段是查詢對應的文檔ID,第二階段再根據文檔ID去查詢完整文檔。而NoSQL一般是第一個階段就會返回結果,而在Es中兩種都支持。

Lucene的讀

在Lucene這個功能是通過IndexSearcher的下面三個接口實現的:

public TopDocs search(Query query, int n);
public Document doc(int docID);
public int count(Query query);

1.search接口實現搜索功能,返回最滿足的N個結果

2.doc接口通過doc id查詢Doc內容

3.count接口通過Query獲取命中數

Elasitcsearch的讀

Es中每個Shard都會有多個副本,主要是爲了保證數據可靠性,還可以在讀流量很大的時候把部分瀏覽分發到副本上。寫的時候可能要寫大部分的Replica Shard,但是查詢的時候只需要查詢Primary和Replica任何一個就行。
在這裏插入圖片描述
寫的時候要寫大部分的副本,但是查詢的時候只需要查詢Primary和Replica任何一個都行。

那如何支持分佈式?

Es中通過Shard實現分佈式,數據寫入的時候,會根據_routing規則把數據寫入某一個Shard中。一個Index可以由多個Shard組成,那麼查詢的時候數據可能會在當前索引的所有分片中,因此查詢的時候是要查詢所有的Shard的。

同一個Shard的Primary和Replica只要選擇一個就行。查詢請求會分發給所有的Shard,每一個Shard都是一個獨立的查詢引擎。舉個例子:假如要返回全局Top N的結果,那麼所有分片都會返回Top N結果,然後在客戶節點裏面會接收到所有分片的結果,再通過一個優先隊列做二次排序,返回Top 10結果返回給用戶。

在這裏插入圖片描述
Es中的查詢主要分爲兩類:

Get請求:通過ID查詢特定的Doc
Search請求: 通過Query查詢匹配的Doc

對於Search類請求,查詢同時會查詢內存和磁盤上的Segment,最後結果合併再返回。這種查詢是近實時的,主要由於內存中的Index數據需要一段時間後纔會refresh到緩存中的Segment

對於Get類請求,查詢的時候會先查詢內存中的TransLog,如果找到之後就立即返回,如果沒有找到就再查詢磁盤上的TransLog,如果還沒有就會再去查詢磁盤上的Segment。這種查詢是實時的,也是爲了保證NoSQL場景下實時性的要求。

在這裏插入圖片描述
搜索系統一般是二階段查詢:
1.二階段查詢先查詢匹配到的DocID,然後再返回DocID對應的完整文檔。

2.一階段查詢直接返回DocID對應的完整文檔。

3.三階段查詢

搜索裏面有一種算分邏輯是根據TF(Term Frequency)和DF(Document Frequency)計算基礎分,但是Elasticsearch中查詢的時候,是在每個Shard中獨立查詢的,每個Shard中的TF和DF也是獨立的,雖然在寫入的時候通過_routing保證Doc分佈均勻,但是沒法保證TF和DF均勻,那麼就有會導致局部的TF和DF不準的情況出現,這個時候基於TF、DF的算分就不準。爲了解決這個問題,Elasticsearch中引入了DFS查詢,比如DFS_query_then_fetch,會先收集所有Shard中的TF和DF值,然後將這些值帶入請求中,再次執行query_then_fetch,這樣算分的時候TF和DF就是準確的,類似的有DFS_query_and_fetch。

Es查詢流程

主要分爲Query和Fetch階段

  • Query階段

簡單來說,因爲所以的數據是分佈在所有分片上的,因此需要把請求分發到所有的分片中,然後會異步等待返回結果,再把結果合併。

舉個例子,加入要查詢全局數據的Top N匹配,那麼是要維護一個Top N大小的優先隊列,每當收到一個shard的返回就把結果放到優先隊列中做一次排序。直到所有的Shard都返回。

翻頁邏輯也是在這裏,如果需要取Top 30~ Top 40的結果,這個的意思是所有Shard查詢結果中的第30到40的結果,那麼在每個Shard中無法確定最終的結果,每個Shard需要返回Top 40的結果給Client Node,然後Client Node中在merge docs的時候,計算出Top 40的結果,最後再去除掉Top 30,剩餘的10個結果就是需要的Top 30~ Top 40的結果。

這裏先介紹一下Es的 from + size 淺分頁

from: 目標數據偏移值
size: 返回的數目

eg: from =20 ,size=10, 即查詢前20條數據,然後截斷前10條,只返回10-20的數據。這樣其實白白浪費了前10條的查詢。而且並不能獲取所有的數據(默認最大記錄數10000),因爲隨着頁數的增加,會消耗大量的內存,導致ES集羣不穩定。

Es提供三種解決深度翻頁的方法:

1.scroll api提供了一個全局深度翻頁的操作, scroll每次只能獲取一頁內容,然後返回一個scroll_id,使用該scroll_id可以順序獲取下一批次的數據;

scroll 請求不能用來做用戶端的實時請求,只能用來做線下大量數據的翻頁處理,例如數據的導出、遷移和_reindex操作

2.sliced scroll
sliced scroll api 除指定上下文保留時間外,還需要指定最大切片和當前切片,最大切片數據一般和shard數一致或者小於shard數,每個切片的scroll操作和scroll api的操作是一致的:

3.search after

search_after 分頁的方式是根據上一頁的最後一條數據來確定下一頁的位置,同時在分頁請求的過程中,如果有索引數據的增刪改查,這些變更也會實時的反映到遊標上。但是需要注意,因爲每一頁的數據依賴於上一頁最後一條數據,所以無法跳頁請求。

如果每次只需要返回10條結構,則每個Shard只需要返回search_after之後的10個結果即可,返回的總數據量只是和Shard個數以及本次需要的個數有關,和歷史已讀取的個數無關

  • Fetch階段

query 階段知道了要取哪些數據,但是並沒有取具體的數據,這就是 fetch 階段要做的。

任何搜索系統中,除了Query階段外,還會有一個Fetch階段,這個Fetch階段在數據庫類系統中是沒有的,是搜索系統中額外增加的階段。搜索系統中額外增加Fetch階段的原因是搜索系統中數據分佈導致的,在搜索中,數據通過routing分Shard的時候,只能根據一個主字段值來決定,但是查詢的時候可能會根據其他非主字段查詢,那麼這個時候所有Shard中都可能會存在相同非主字段值的Doc,所以需要查詢所有Shard才能不會出現結果遺漏。同時如果查詢主字段,那麼這個時候就能直接定位到Shard,就只需要查詢特定Shard即可,這個時候就類似於數據庫系統了

基於上述原因,第一階段查詢的時候並不知道最終結果會在哪個Shard上,所以每個Shard中管都需要查詢完整結果,比如需要Top 10,那麼每個Shard都需要查詢當前Shard的所有數據,找出當前Shard的Top 10,然後返回給Client Node。如果有100個Shard,那麼就需要返回100 * 10 = 1000個結果,而Fetch Doc內容的操作比較耗費IO和CPU,如果在第一階段就Fetch Doc,那麼這個資源開銷就會非常大。所以,一般是當Client Node選擇出最終Top N的結果後,再對最終的Top N讀取Doc內容。通過增加一點網絡開銷而避免大量IO和CPU操作,這個折中是非常划算的

在這裏插入圖片描述

ELK

  • Logstash

INPUT:
如本地文件,日誌等

FILTER:
負責數據的預處理

OUTPUT:
數據輸出

索引生命週期管理

對於日誌數據,由於單個索引的存儲量的瓶頸,ES一般推薦使用時間作爲後綴爲同一份日誌數據創建多個索引。

可以基於時間來對索引數據分爲四個階段:

Hot:索引數據被大量更新與新增,並且用戶對處於該階段的索引由很強的查詢需求(熱數據)
Warm:索引數據不再被更新,單用戶對處於該階段的索引任有查詢需求
Cold:索引數據不再被更新,用戶對這個階段的索引查詢需求較低並且可以容忍較大的查詢延遲
Delete:數據不再需要,索引可被刪除

Lucene 查詢原理

  • Index: 索引,由很多Document組成,相當於關係型數據庫一個表

  • Document: 由很多Field組成,相當於表中的一行

  • Field: 由很多Term組成,包括Field Name和Field Value

  • Term 由很多字節組成,Text類型的Field Value分詞之後對每個最小單元叫term

Lucene查詢過程

在lucene中查詢基於segment,每一個segment都可以看作是一個獨立的subindex。

建立索引過程中,lucene會不斷flush內存中的數據,持久化而形成新的segment。

多個segment也會不斷merge成一個大的segment,在老的segment在查詢在讀取的時候,不會被刪除,而沒有被讀取且被merge的segment會被刪除。

舉個例子:

在這裏插入圖片描述
爲了查詢name=XXX這麼一個條件,會建立基於name的倒排表:
在這裏插入圖片描述
爲了查詢age=XXX這麼一個條件,會建立基於age的倒排表:
在這裏插入圖片描述
那如果term很多,總得需要用一定的數據結構來組織這些term。

所以lucene引入了term dict的概念,在這個dict可以對term排序,然後通過二分查找就可以找到這個term所在的地址。

而爲了實現範圍查詢以及前綴搜索,lucene會使用FST數據結構來存儲term字典。

FST

在此之前,我們總結一下有哪些數據結構適合來做字典?

數據結構 狀態
Array/List 二分法不平衡
HashMap/TreeMap 性能高,內存損耗大
Skip List 適合高併發場景
Trie 公共前綴樹
Double Array Trie 適合做中文詞典
三叉樹 每一個node有3個節點,兼具省空間和查詢快的優點
FST 有限狀態轉移機
  • Finite State Transducer

1)空間佔用小。通過對詞典中單詞前綴和後綴的重複利用,壓縮了存儲空間;
2)查詢速度快。O(len(str))的查詢時間複雜度。

無權FST的構建過程:

  • 插入"cat"
    在這裏插入圖片描述
  • 插入"deep"
    在這裏插入圖片描述
  • 插入"do",與deep做最大前綴匹配

在這裏插入圖片描述

  • 插入"dogs"

在這裏插入圖片描述

以下是有權重FST的構建,字母后面的數字代表權重
在這裏插入圖片描述

FST在單term查詢上可能相比hashmap並沒有明顯優勢,甚至會慢一些。但是在範圍,前綴搜索以及壓縮率上都有明顯的優勢。

跳錶

我們先看一下倒排鏈表的存儲結構:

引入跳錶是爲了能夠快速查找文檔id:

元素排序的,對應到我們的倒排鏈,lucene是按照docid進行排序,從小到大。
跳躍有一個固定的間隔,這個是需要建立SkipList的時候指定好,例如下圖以間隔是3
SkipList的層次,這個是指整個SkipList有幾層

在這裏插入圖片描述

有了這個SkipList以後比如我們要查找docid=12,原來可能需要一個個掃原始鏈表,1,2,3,5,7,8,10,12。有了SkipList以後先訪問第一層看到是然後大於12,進入第0層走到3,8,發現15大於12,然後進入原鏈表的8繼續向下經過10和12。

有了FST和SkipList的介紹以後,我們大體上可以畫一個下面的圖來說明lucene是如何實現整個倒排結構的:

在這裏插入圖片描述

總結:

1.利用FST 組織term index,高效壓縮索引結構,可以保存在內存中,減少磁盤IO次數

2.跳錶組織倒排表,支持O(lgN)查詢文檔ID

倒排合併

如果要進行組合索引的查詢,如果使用傳統的二級索引方案,可能需要建立兩張索引表,然後分別查詢結果後進行合併。

如果age = 18結果過多的話,查詢合併會很耗時。

在這裏插入圖片描述

在termA開始遍歷,得到第一個元素docId=1
Set currentDocId=1
在termB中 search(currentDocId) = 1 (返回大於等於currentDocId的一個doc),
因爲currentDocId ==1,繼續
如果currentDocId 和返回的不相等,執行2,然後繼續

到termC後依然符合,返回結果
currentDocId = termC的nextItem
然後繼續步驟3 依次循環。直到某個倒排鏈到末尾。

感覺就是依次遍歷三個鏈表然後做合併,果某個鏈很短,會大幅減少比對次數,並且由於SkipList結構的存在,在某個倒排中定位某個docid的速度會比較快不需要一個個遍歷。可以很快的返回最終的結果。lucene合併過程中的優化減少了讀取數據的IO。

如何實現返回結果進行排序聚合

1.把搜索過的field放入內存中,可以減少重複IO,但問題是會佔用較多的內存

2.引入DocValues,這是一個基於docid的列式存儲。當我們拿到一系列的docid後,進行排序就可以使用這個列式存儲,結合一個堆排序進行。

Es的分佈式探討

Es集羣構成

節點可以分爲:

DataNode

較高配置服務器, 主要消耗磁盤,內存

作用
1.存儲索引數據
2.對文檔進行增刪改查,聚合操作

node.data=true,表示這個節點是數據節點,會存儲分配在該node上的shard的數據並且負責shard的寫入,查詢等。

MasterNode

普通服務器即可(CPU 內存 消耗一般)

索引數據和搜索查詢等操作會佔用大量的cpu,內存,io資源,爲了確保一個集羣的穩定,分離主節點和數據節點是一個比較好的選擇

作用
1.索引的創建或刪除
2.跟蹤哪些節點是集羣的一部分
3.決定哪些分片分配給相關的節點
注意,node.maser=true只是表示這個節點是master的候選節點,可以參與選舉。

client/路由節點

作用:

1.接受請求並進行轉發,結果聚合
2.分發索引

那如果node.data=false & node.master=false,那麼這個節點可以接受請求並且進行轉發,結果聚合等。

在這裏插入圖片描述

Es的節點發現和選主

本節點到每個hosts中的節點建立一條邊,當整個集羣所有的node形成一個聯通圖時,所有節點都可以知道集羣中有哪些節點,不會形成孤島。

(通過形成一個全連通圖)

爲了避免產生腦裂,Es保證選舉出來的master被多數的master-eligible node認可爲master節點.

master選舉誰來發起,什麼時候發起?

該master-eligible節點的當前狀態不是master。該master-eligible節點通過ZenDiscovery模塊的ping操作詢問其已知的集羣其他節點,沒有任何節點連接到master。包括本節點在內,當前已有超過minimum_master_nodes個節點沒有連接到master。

即當一個節點發現包括自己在內的多數派的master-eligible節點認爲集羣沒有master時,就可以發起master選舉。

選舉出哪個節點?

對所有候選節點進行排序

public MasterCandidate electMaster(Collection<MasterCandidate> candidates) {
        assert hasEnoughCandidates(candidates);
        List<MasterCandidate> sortedCandidates = new ArrayList<>(candidates);
        sortedCandidates.sort(MasterCandidate::compare);
        return sortedCandidates.get(0);
    }

按照什麼順序進行排序?

public static int compare(MasterCandidate c1, MasterCandidate c2) {
    // we explicitly swap c1 and c2 here. the code expects "better" is lower in a sorted
    // list, so if c2 has a higher cluster state version, it needs to come first.
    int ret = Long.compare(c2.clusterStateVersion, c1.clusterStateVersion);
    if (ret == 0) {
        ret = compareNodes(c1.getNode(), c2.getNode());
    }
    return ret;
}

1.當clusterStateVersion越大,優先級越高。這是爲了保證新Master擁有最新的clusterState(即集羣的meta),避免已經commit的meta變更丟失。因爲Master當選後,就會以這個版本的clusterState爲基礎進行更新。(一個例外是集羣全部重啓,所有節點都沒有meta,需要先選出一個master,然後master再通過持久化的數據進行meta恢復,再進行meta同步)。

2.當clusterStateVersion相同時,節點的Id越小,優先級越高。即總是傾向於選擇Id小的Node,這個Id是節點第一次啓動時生成的一個隨機字符串。

如何保證不腦裂?

本質上是過半原則.

但是這種原則可能會有問題:

因爲上述流程並沒有限制在選舉過程中,一個Node只能投一票,那麼什麼場景下會投兩票呢?比如NodeB投NodeA一票,但是NodeA遲遲不成爲Master,NodeB等不及了發起了下一輪選主,這時候發現集羣裏多了個Node0,Node0優先級比NodeA還高,那NodeB肯定就改投Node0了。假設Node0和NodeA都處在等選票的環節,那顯然這時候NodeB其實發揮了兩票的作用,而且投給了不同的人。

解決方法:

Raft算法引入了選舉週期的概念,可以保證每個選舉週期的每個成員只能投一票,如果需要再投就會進入下一個選舉週期term+1

假如出現了兩個節點都認爲自己是master,那麼其中一個的term一定會大於另一個的term,默認任期大的纔會成爲leader.這就保證了任期小的節點不會commit,從而保證了集羣的狀態變更是一致的.

但Es並不是使用Raft來做分佈式一致性維護的,這也是它的一種缺陷.

與Zookeeper,raft等實現方式進行比較

  • 與Zookeeper相比

1.節點發現:
每當一個新節點加入集羣,到Zookeeper中的某一個目錄註冊一個臨時的znode,然後master節點監聽這個目錄,註冊一個臨時的znode. 如果master節點監聽到這個目錄的子節點有增減,那麼發現有新節點的時候,就可以把新節點加入集羣.

2.master選舉:
就是搶鎖,搶鎖有兩種思路:

一個節點啓動的時候,嘗試去固定位置創建一個名爲master的臨時節點,創建成功就相當於搶鎖成功.然後其他節點監聽這個master節點,一旦節點下線,那麼其他節點就會嘗試去創建.

.一個節點啓動的時候,在指定目錄下創建一個帶有序號的znode,然後比較目錄下所有znode,看znode是否是索引最小的.如果是則搶鎖成功.否則監聽比自己更小的節點,一旦發現更小的節點下線,重複上述過程.

3.錯誤檢測:

由於節點的znode和master的znode都是臨時的znode,如果節點發生故障,會與Zookeeper斷開session,znode下線,那麼master只需要監聽znode的變更就知道datanode的情況.

同樣如果master掉線,datanode只需要監聽master znode被刪除的事件即可,然後就嘗試成爲新的master.

  • 與Raft相比

與es對比:

多數派原則:必須得到超過半數的選票才能成爲master。選出的leader一定擁有最新已提交數據:在raft中,數據更新的節點不會給數據舊的節點投選票,而當選需要多數派的選票,則當選人一定有最新已提交數據。在es中,version大的節點排序優先級高,同樣用於保證這一點。

Master如何管理集羣

1.如何新建或者刪除索引?
2.如何對分片進行重新調度?實現負載均衡?

所以Master節點必須要以某種方式通知其他節點,從而讓其他節點執行相應的動作,來完成某些事情.比如建立一個新的index就需要把其Shard分配在其他節點上,在這些節點上創建出對應的Shard目錄,並且在內存中創建對應的Shard結構.

在Es中,Master節點是通過發佈ClusterState來通知其他節點的,然後Data節點根據接收到的ClusterState來判斷要執行的操作. 總結而言這是通過主節點傳輸元數據來驅動各個模塊的工作.

如這個過程中Master掛掉了,那麼可能只有部分節點按照新的Meta執行了操作。當選舉出新的Master後,需要保證所有節點都要按照最新的Meta執行操作,不能回退,因爲已經有節點按照新的Meta執行操作了,再回退就會導致不一致。只要新Meta在一個節點上被commit,那麼就會開始執行相應的操作。因此我們要保證一旦新Meta在某個節點上被commit,此後無論誰是master,都要基於這個commit來產生更新的meta,否則就可能產生不一致.

元數據組成,存儲與恢復

這一部分直接摘抄

1.Meta:ClusterState、MetaData、IndexMetaData
Meta是用來描述數據的數據。在ES中,Index的mapping結構、配置、持久化狀態等就屬於meta數據,集羣的一些配置信息也屬於meta。這類meta數據非常重要,假如記錄某個index的meta數據丟失了,那麼集羣就認爲這個index不再存在了。ES中的meta數據只能由master進行更新,master相當於是集羣的大腦。
ClusterState

集羣中的每個節點都會在內存中維護一個當前的ClusterState,表示當前集羣的各種狀態。ClusterState中包含一個MetaData的結構,MetaData中存儲的內容更符合meta的特徵,而且需要持久化的信息都在MetaData中,此外的一些變量可以認爲是一些臨時狀態,是集羣運行中動態構建出來的。

ClusterState內容包括:
long version: 當前版本號,每次更新加1
String stateUUID:該state對應的唯一id
RoutingTable routingTable:所有index的路由表
DiscoveryNodes nodes:當前集羣節點
MetaData metaData:集羣的meta數據
ClusterBlocks blocks:用於屏蔽某些操作
ImmutableOpenMap<String, Custom> customs: 自定義配置
ClusterName clusterName:集羣名

MetaData
上面提到,MetaData更符合meta的特徵,而且需要持久化,那麼我們看下這個MetaData中主要包含哪些東西:

MetaData中需要持久化的包括:
String clusterUUID:集羣的唯一id。
long version:當前版本號,每次更新加1
Settings persistentSettings:持久化的集羣設置
ImmutableOpenMap<String, IndexMetaData> indices: 所有Index的Meta
ImmutableOpenMap<String, IndexTemplateMetaData> templates:所有模版的Meta
ImmutableOpenMap<String, Custom> customs: 自定義配置

我們看到,MetaData主要是集羣的一些配置,集羣所有Index的Meta,所有Template的Meta。下面我們再分析一下IndexMetaData,後面還會講到,雖然IndexMetaData也是MetaData的一部分,但是存儲上卻是分開存儲的。

IndexMetaData
IndexMetaData指具體某個Index的Meta,比如這個Index的shard數,replica數,mappings等。

IndexMetaData中需要持久化的包括:
long version:當前版本號,每次更新加1。
int routingNumShards: 用於routing的shard數, 只能是該Index的numberOfShards的倍數,用於split。
State state: Index的狀態, 是個enum,值是OPEN或CLOSE。
Settings settings:numbersOfShards,numbersOfRepilicas等配置。
ImmutableOpenMap<String, MappingMetaData> mappings:Index的mapping
ImmutableOpenMap<String, Custom> customs:自定義配置。
ImmutableOpenMap<String, AliasMetaData> aliases: 別名
long[] primaryTerms:primaryTerm在每次Shard切換Primary時加1,用於保序。
ImmutableOpenIntMap<Set> inSyncAllocationIds:處於InSync狀態的AllocationId,用於保證數據一致性,下一篇文章會介紹。

MetaData是由Master管理的,爲什麼DataNode上也要保存MetaData呢?主要原因是考慮到數據的安全性,很多用戶沒有考慮Master節點的高可用和數據高可靠,在部署ES集羣時只配置了一個MasterNode,如果這個節點不可用,就會出現Meta丟失,後果非常嚴重。

Meta的恢復

假設ES集羣重啓了,那麼所有進程都沒有了之前的Meta信息,需要有一個角色來恢復Meta,這個角色就是Master。所以ES集羣需要先進行Master選舉,選出Master後,纔會進行故障恢復。

當Master選舉出來後,Master進程還會等待一些條件,比如集羣當前的節點數大於某個數目等,這是避免有些DataNode還沒有連上來,造成不必要的數據恢復等。

當Master進程決定進行恢復Meta時,它會向集羣中的MasterNode和DataNode請求其機器上的MetaData。對於集羣的Meta,選擇其中version最大的版本。對於每個Index的Meta,也選擇其中最大的版本。然後將集羣的Meta和每個Index的Meta再組合起來,構成當前的最新Meta。

Es的一致性問題

在這裏插入圖片描述
Master發送commit的原則是隻要超過半數的候選節點收到了最新的元數據就會提交.所以是隻要認爲超過半數節點收到了最新的元數據,這個CLusterState就一定會被提交,但二階段提交協議存在一定問題:

第一階段master節點會發送新的ClusterState,接收到的節點只是會把新的ClusterState放入內存的一個隊列然後就返回ack,並沒有做持久化.

如果master在commit階段,只commit了少數幾個節點就出現了網絡分區,將master與這幾個少數節點分在了一起,其他節點可以互相訪問。此時其他節點構成多數派,會選舉出新的master,由於這部分節點中沒有任何節點commit了新的ClusterState,所以新的master仍會使用更新前的ClusterState,造成Meta不一致。或者是說Master節點在commit了部分幾個節點之後就宕機,部分節點有最新的元數據,

解決方法

1.實現一個標準的一致性算法如Raft

Raft的follower在接收到日誌之後就會進行持久化落盤,而節點接收到ClusterState只是放到內存的一個隊列中返回,不會持久化.

Raft可以保證在超過半數節點應答之後,這條日誌一定可以被commit,而ES中沒有保證這一點,所以會存在一致性問題.

2.引入Zookeeper來保存元數據,但ZK是基於內存的數據模型,如果元數據過大,在性能上可能會下降

3.使用一個共享存儲來保存元數據

數據節點的一致性

Es的Index會劃分爲多個分片存儲在不同的節點上,對於每個Shard,又會有多個Shard的副本,其中一個爲Primary,其餘的一個或多個爲Replica。數據在寫入時,會先寫入Primary,由Primary將數據再同步給Replica。在讀取時,爲了提高讀取能力,Primary和Replica都會接受讀請求。
在這裏插入圖片描述
ES的特性:

1.數據高可靠:數據有多個副本
2.高可用:Primary宕機,也可以從Replica選出新的Primary提供服務
3.讀能力擴展: Primary和Replica都可以承擔讀請求
4.故障恢復能力,Primary或者副本掛掉的時候,可以由新的Primary通過複製數據產生新的副本.

問題:

1.數據怎麼從Primary複製到Replica?
2.一次寫入要求所有副本都成功嗎?
3.如果主副本掛掉,會不會丟數據?
4.如果數據讀副本Shard,能夠保證總是讀到最新數據嗎?
5.故障恢復的時候,是否需要拷貝分片下的所有數據?

宏觀來看:從Priamy->Replica

1.檢查存活的分片數量
2.寫入Primary分片
3.併發向所有副本發起寫入請求
4.等所有Replica返回或者失敗的時候,再把結果返回給客戶端.(與Kafka的異步更新有些不同,但是一致性不好)

從Primary自身角度來看:

1.爲什麼要寫translog?

類似於數據庫中的提交日誌或者binlog,只要translog寫入並且成功flsuh,相當於把操作記錄落盤.這裏我們先回顧索引寫入分片的過程:
1.先寫入Lucene的Index Buffer,這個時候不能被搜索
2.如果這個時候直接落盤的話,會涉及到系統調用fsync,比較損耗性能,因此可以寫到文件系統的cache中,這個過程也叫refresh,只有轉化成緩存中的segment才能被搜索到,refresh間隔默認是1s,所以Es是近實時的搜索
3.但是萬一segment數據落盤之前es進程宕機了會丟數據對吧,因此需要寫入lucene後再把數據更改記錄和順序寫到translog中

那麼即使宕機了,也可以根據translog來恢復數據,segment就可以晚一點落盤.

而且translog是append寫而不是隨機寫,因此寫入效率其實也不低.

總結而言:translog作用:

保證數據安全,節點重啓後可以恢復沒有落盤的數據,一旦落盤之後,對應的translog也會被刪除掉.

可以用於Replica和Primary副本的數據同步

2.爲什麼先寫Lucene再寫translog?

正常來講分佈式系統是先寫如commitLog,再更新內存.但是Es是反過來,主要原因大概是寫入Lucene時,Lucene會再對數據進行一些檢查,有可能出現寫入Lucene失敗的情況。如果先寫translog,那麼就要處理寫入translog成功但是寫入Lucene一直失敗的問題,所以ES採用了先寫Lucene的方式.

引入Term和SequenceNumber

Term:任期,可以理解爲每次Primary變更的時候都會加1
SequenceNumber: 序列號,可以理解爲每一次操作後都會加1

寫請求是發給Primary的,所以Term和SequenceNumber會由Primary分配,在向Replica發送同步請求的時候,會帶上這兩個值.

LocalCheckpoint和GlobalCheckpoint

LocalCheckpoint代表本Shard中所有小於這個值請求都已經處理完畢.

GlobalCheckpoint代表所有小於這個值的請求在所有的副本上都已經正確處理了.GlobalCheckpoint會由Primary進行維護,每個Replica會向Primary彙報自己的LocalCheckpoint,Primary根據這些信息來維護GlobalCheckpoint.

快速故障恢復

實現故障恢復功能需要考慮的點:

1.需要保存故障期間所有操作機器順序保存下來

可以用translog實現

2.需要知道從哪一個點開始恢復數據

可以用checkpoint實現

基於Lucene查詢原理分析Es性能

1.FST: 一種類似於前綴樹的結構,用於維護FST的字典,可以在FST上實現單Term,Term範圍或者前綴,通配符查詢

2.倒排鏈:保存了每個term對應的docID的列表,採用跳錶來維護倒排鏈表,實現log(N)時間複雜度的查詢

3.BKD-Tree: 一種保存多維空間點的數據結構,用於數值類型快速查找

4.DocValues: 基於docid的列式存儲,可以提高排序聚合的性能

查詢結果的合併

1.對單個詞條進行查詢,Lucene會讀取詞條的倒排鏈,然後返回所有的文檔ID

2.對字符串進行範圍/前綴/通配符查詢,Lucene會從FST獲取所有符合條件的Term,然後根據這些Term的倒排表來做合併操作,合併可以嘗試考慮BitSet,一個文檔ID對應一個Bit,而BitSet大小取決於文檔數目的多少

3.如果是對數字類型進行範圍查詢,Lucene會通過BKD-Tree查找到符合條件的docID的集合,但是這個集合中docID並非有序,因此先要構造BItSet或者轉成一個有序數組.

查詢搜索簡單結論:

1.掃描倒排鏈的效率很高
2.跳錶進行倒排鏈合併的時候,性能取決於最短鏈的掃描次數和skip的次數,因爲skiplist是以BLOCK爲單位進行組織的,每一次skip要涉及到一次BLOCK的順序掃描
3.FST進行前綴後綴和通配符的查詢會比較慢
4.BKD-Tree的範圍查詢比FST效率高,但是因爲其返回的docID不是有序,如果要和其他查詢做交集,必須先構造BItSet,而構造BitSet是比較耗時的.

IndexSorting:

大意就是查詢的時候如果已經返回了所有的數據,就可以提前中斷.

什麼是 IndexSorting?

IndexSorting是一種預排序,一個segment內的文檔會按照特定的順序進行排序,舉個例子,假如文檔中有一列爲Timestamp,那麼在IndexSorting中設置按照Timestamp逆序排序,那麼在一個Segment內,docID越小(docID是按照寫入順序進行分配的),對應的文檔的Timestamp越大,即按照Timestamp從大到小的順序分配docID。

使用場景?

IndexSorting提高了數據壓縮率

Lucene中使用了很多的壓縮算法來對數據進行壓縮,壓縮一方面減少了磁盤使用量,另一方面也減少了查詢時讀取磁盤的數據量和IO次數等,對查詢性能有很大幫助。

Lucene中同時包括行存(Store)與列存(DocValues)的存儲方式,但不管是行存還是列存,當應用IndexSorting後,相鄰數據的相似度就會越高,也就越利於壓縮。這不僅僅是體現在排序字段上,也體現在其他字段的相似度上。比如時序場景,當按照時間排序後,各個Metrics的值的近似度也會越大。所以IndexSorting可以提高數據的壓縮率。

爲啥IndexSorting可以優化性能?

  • 提前中斷

當查詢的Sort順序與IndexSorting順序相同並且不需要獲取符合條件的記錄總數的時候,可以實現"提前中斷"

舉個例子,假如IndexSorting中設置docID的排列順序是按照時間戳降序排序,而搜索的順序也剛好是降序排序.

那麼查詢前100個符合條件的結果就可以返回,因爲這100個肯定就是時間戳最大的100個.

提前中斷可以極大的提升查詢性能,特別是當一個查詢條件命中的文檔數量非常多的時候。在沒有IndexSorting時,必須把所有符合條件的文檔的docID掃描一遍,並且讀取這些doc的一些字段來排序,選出符合條件的doc。有了IndexSorting之後,只需要選出前Top個doc即可,避免了全部掃描,性能甚至可以提升幾個數量級.

IndexSorting提高了數據壓縮率

Lucene中使用了很多的壓縮算法來對數據進行壓縮,壓縮一方面減少了磁盤使用量,另一方面也減少了查詢時讀取磁盤的數據量和IO次數等,對查詢性能有很大幫助。

Lucene中同時包括行存(Store)與列存(DocValues)的存儲方式,但不管是行存還是列存,當應用IndexSorting後,相鄰數據的相似度就會越高,也就越利於壓縮。這不僅僅是體現在排序字段上,也體現在其他字段的相似度上。比如時序場景,當按照時間排序後,各個Metrics的值的近似度也會越大。所以IndexSorting可以提高數據的壓縮率。

IndexSorting如何實現?

1.Lucene要保證生成segment的時候內部數據有序.

每個doc寫入的時候會按照寫入順序分配一個docID,然後依次處理其涉及到的倒排索引,正排索引,store fields等.

因爲doc values 正排索引是列式存儲,那麼按照某個列進行排序的時候其實是對Doc Values做排序操作.

而且如果順序發生了變化,是要store field和term vector,需要從文件中讀出來,重新排列後再寫到一個新文件裏,原來的文件就相當於一個臨時文件。對於內存中的數據結構,直接在內存中重排後寫到文件中。這裏就涉及到了磁盤IO.

2.要保證Merge操作生成的新Segment是有序的

直接跑個歸併就行.

小結:

IndexSorting通過把segment裏面的文檔ID根據特定規則進行預排序從而實現提前中斷,進而達到大大減少所要掃描的數據量的效果.

附帶的優化效果就是可以提高壓縮率,減少存儲空間.

但是缺點是因爲預排序的時候性能消耗比較大,所以這個Trick對於着重寫入性能的場景是不適用的.

調優

建立索引效率優化(寫入優化)

1.批量提交

Logstash可以設置batch size,會把一個批量請求分爲多個批量請求.

2.優化硬件

  • 在經濟壓力能承受範圍內,儘量使用SSD
  • 磁盤備份用RAID0,因爲 Elasticsearch 在自身層面通過副本,已經提供了備份的功能,所以不需要利用磁盤的備份功能

3.增加refresh時間間隔

默認refresh時間間隔1s,每次refresh實際上要進行segment的合併和生成,從內存寫入到文件系統的緩存.那麼適當增加刷寫的頻率可以減少磁盤壓力.

4.根據業務場景減少副本數量

文檔寫入主副本的時候,同時也會寫入從副本,只有在全部Replica都獲取到最新的數據的時候這個文檔才能夠被搜索.那麼在ELK日誌系統中其實可以適當減少副本的數量.

寫入前,副本數設置爲0,
寫入後,副本數設置爲原來值。

5.6.7點都是從降低存儲成本入手思考

5.把不需要建立索引的字段直接刪掉,合併冗餘字段

6.降低存儲成本,字段存儲

在這裏插入圖片描述Es的索引數據其實分成三份:

1.index 倒排索引,用來做檢索

2._source 行存儲,原始數據

3.doc_values 列存,用於針對某列的排序聚合操作

針對業務場景選擇不存某種數據,例如日誌數據,對排序聚合要求不高,可以不用存列存儲.

7.降低存儲成本,從字符串字段格式思考

在這裏插入圖片描述

  • text
    會做分詞,每個詞都可以建立倒排索引.如果僅僅是想把這個文本存進去並且取出來的話,後面都是可以去掉。

  • keyword
    不分詞,相對於text存儲成本低,寫入速率高.

因此可以把一些不需要分詞的字段設置爲keyword,降低其存儲的成本

除了上面幾種方法,還可以從以下幾個角度思考如何降低存儲成本:

1.冷熱分離,很久不用的數據直接從內存落盤.

2.降低倒排索引內存,因爲fst其實是常駐JVM的,因此可以考慮如何優化這個內存大小.

在這裏插入圖片描述FST的term index不是指向那個Term Dict嘛,那麼可以做分塊處理,因爲FST實際上對前綴和後綴都做了收斂處理,那麼把前綴或者後綴相同的劃分爲同一個block,那麼就可以減少FST中節點的數量.

或者也可以開闢堆外內存.

8.使用路由

路由就是對文檔id取哈希值然後取模,那麼routing相同的數據會指定寫入到同一個分片中.那麼查詢的時候可以指定routing策略,只會查詢相應存儲數據的分片,減少調度開銷.

9.讓index儘量分攤到多個節點上

避免過熱節點的出現,影響寫入和查詢性能

10.從查詢方式優化

使用query-bool-filter
在這裏插入圖片描述Es默認查詢方式是數據搜到之後,根據數據元統計一個分數,然後對分數做一個排序.這個分數作爲查詢結果和查詢條件的相關性度量而使用query-bool-filter可以避免打分,並且結果集可以緩存.

JVM配置&集羣規劃優化

1.Es默認堆內存是1GB,但堆內存也並不是設置的越大越好.

因爲Lucene也要利用內存,要在緩存中存儲這些segment,如果Es進程佔的堆內存太大,那麼會導致Lucene無法充分利用緩存,會影響全文檢索的性能.

同時JVM的內存不要超過32G,jvm在內存小於32G的時候會採用一個內存對象指針壓縮技術。

假如一臺64G內存的機器,堆內存一般設置爲31G,剩下的都分給lucene的緩存利用就好.

2.用G1代替CMS回收算法

3.集羣節點數:<=3,建議:所有節點的master:true, data:true。既是主節點也是路由節點。
集羣節點數:>3, 根據業務場景需要,建議:逐步獨立出Master節點和協調/路由節點。

路由查詢

Es如何知道一個文檔應該存放在那個分片?

可以計算哈希然後取模
shard = hash(docID) % num_of_shards

查詢可以分爲帶routing查詢和不帶routing查詢

不帶routing查詢

  • 分發: 請求到達某個協調節點上之後,協調節點會把查詢請求分發到每個分片上
  • 聚合: 各個分片上的查詢結果會聚集在協調節點上,協調節點可以維護個堆,然後對其進行排序

routing查詢

不需要查詢所有的Shard,也不需要把所有結果返回給協調節點進行排序.直接定位到相應的分片即可

解決深翻頁問題:

舉個例子,假如要返回全局第40 數據,要從所有分片返回Top40的數據做排序,然後最後返回全局Top40的數據.

如果數據量一大,對內存,CPU的壓力很大很容易OOM

  • scroll 遊標搜索
    並不適合實時搜索,更適合後臺批處理任務
    具體步驟:
    1.初始化,把所有符合搜索條件的結果保存快照
    2.遍歷搜索的時候從這個快照取數據
    3.初始化後對索引插入,刪除,更新數據也不會影響遍歷結果

  • search_after

searchAfter()方法原理是獲取上一頁的最後一個元素和pageSize,再從最後一個元素的後一個開始取pageSize條數據

Es和MongoDB對比

MongoDB和其他RDBMS是競爭關係
而es的場景是用其他數據庫作爲主要的數據存儲,然後使用Es來做數據檢索

相同點

1.都是以json格式管理數據
2.都支持CRUD
3.都支持聚合和全文搜索
4.對事務的支持都不強
5.都支持分片和複製,不過

不同點

1.Es的分片支持hash,而mongo的分片支持Hash和Range
2.Es是天生的分佈式,主副分片自動分配和複製開箱即用。而mongodb的分佈式是由"前置查詢路由+配置服務+shard集合"
3.Es的內部存儲是"倒排索引index+列存儲docvalues+fielddata行存儲"
4.es字段是自動索引,mongodb是手動索引

總結:

1.es更偏向於檢索,查詢和數據分析即OLAP
2.mongodb偏向於大規模數據下的CRUD,對事務性要求不強的OLTP

ref

騰訊的ES調優經驗
阿里的ES底層探討

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章