【Elasticsearch實踐】Elasticsearch爲什麼這麼快

建議先自行學習偏基礎的 Elasticsearch 知識內容:
Elasticsearch基礎

思考幾個問題:

  • 爲什麼搜索是 近實時 的?
  • 爲什麼文檔的 CRUD (創建-讀取-更新-刪除) 操作是實時的?

前言:ES整體架構

1、集羣cluster
在這裏插入圖片描述
2、節點Node:就是個機器
在這裏插入圖片描述
3、由一個或者多個節點,多個綠色小方塊組合在一起形成一個ElasticSearch的索引
在這裏插入圖片描述
4、在一個索引下,分佈在多個節點裏的綠色小方塊稱爲分片:Shard
在這裏插入圖片描述
5、一個分片就是一個Lucene Index
在這裏插入圖片描述
6、在Lucene裏面有很多小的Segment,即爲存儲的最小管理單元在這裏插入圖片描述
我們分別從Node維度、Shard維度、Segment維度來闡明什麼Elasticsearch這麼快。

一、Node節點維度

多節點的集羣方案,提高了整個系統的併發處理能力。

多節點的集羣方案

路由一個文檔到一個分片中:當索引一個文檔的時候,文檔會被存儲到一個主分片中。 Elasticsearch 如何知道一個文檔應該存放到哪個分片中呢?實際上,這個過程是根據下面這個公式決定的:

shard = hash(routing) % number_of_primary_shards

routing 是一個可變值,默認是文檔的 _id ,也可以設置成一個自定義的值。這就解釋了爲什麼我們要在創建索引的時候就需要確定好主分片的數量,並且永遠不會改變這個數量:因爲如果數量變化了,那麼所有之前路由的值都會無效,文檔也再也找不到了。確定了在哪個分片中,繼而可以判定其在哪個節點上。

那麼主分片數確定的情況下,如果做集羣擴容呢?
下圖是一種主分片的擴容辦法,開始設置爲5個分片,在單個節點上,後來擴容到5個節點,每個節點有一個分片。也就是說單個分片的容量變大了,但是分片數量並不增加。
在這裏插入圖片描述

協調節點

節點分爲主節點(Master Node)、數據節點(Data Node)和客戶端節點(Client Node,單純爲了做請求的分發和彙總)。每個節點都可以接受客戶端的請求,每個節點都知道集羣中任一文檔位置,所以可以直接將請求轉發到需要的節點上。當接受請求後,節點變爲【協調節點】。從這個角度,整個系統可以接受更高的併發請求,當然搜索的就更快了。

以更新文檔爲例:
在這裏插入圖片描述

  • 客戶端向 Node 1 發送更新請求。
  • 它將請求轉發到主分片所在的 Node 3 。
  • Node 3 從主分片檢索文檔,修改 _source 字段中的 JSON ,並且嘗試重新索引主分片的文檔。 如果文檔已經被另一個進程修改,它會重試步驟 3 ,超過 retry_on_conflict 次後放棄。
  • 如果 Node 3 成功地更新文檔,它將新版本的文檔並行轉發到 Node 1 和 Node 2 上的副本分片,重新建立索引。 一旦所有副本分片都返回成功, Node 3 向協調節點也返回成功,協調節點向客戶端返回成功。

樂觀併發控制

Elasticsearch 中使用的這種方法假定衝突是不可能發生的,並且不會阻塞正在嘗試的操作。因爲沒有阻塞,所以提升了索引的速度,同時可以通過_version字段來保證併發情況下的正確性:

PUT /website/blog/1?version=1 
{
  "title": "My first blog entry",
  "text":  "Starting to get the hang of this..."
}

控制在我們索引中的文檔只有現在的_version爲 1 時,本次更新才能成功。

二、Shard分片維度

副本分片

可以設置分片的副本數量來提升高併發場景下的搜索速度,但是同時會降低索引的效率。

Segment的不變性

在底層採用了分段的存儲模式,使它在讀寫時幾乎完全避免了鎖的出現,大大提升了讀寫性能。

  • 不需要鎖。如果你從來不更新索引,你就不需要擔心多進程同時修改數據的問題。
  • 一旦索引被讀入內核的文件系統緩存,便會留在哪裏,由於其不變性。只要文件系統緩存中還有足夠的空間,那麼大部分讀請求會直接請求內存,而不會命中磁盤。這提供了很大的性能提升。
  • 其它緩存(像filter緩存),在索引的生命週期內始終有效。它們不需要在每次數據改變時被重建,因爲數據不會變化。
  • 寫入單個大的倒排索引允許數據被壓縮,減少磁盤 I/O 和 需要被緩存到內存的索引的使用量。

怎樣在保留不變性的前提下實現倒排索引的更新?
即用上文提到的_version,創建更多的索引文檔。實際上一個 UPDATE 操作包含了一次 DELETE 操作(僅記錄標誌待Segment Merge 的時候才真正刪除)和一次 CREATE 操作。

提升寫入速度

爲了提升寫索引速度,並且同時保證可靠性,Elasticsearch 在分段的基礎上,增加了一個 translog ,或者叫事務日誌,在每一次對 Elasticsearch 進行操作時均進行了日誌記錄。

1、一個文檔被索引之後,就會被添加到內存緩衝區,並且追加到了 translog:
在這裏插入圖片描述
2、分片每秒被刷新(refresh)一次
在這裏插入圖片描述

  • 這些在內存緩衝區的文檔被寫入到一個新的段中,且沒有進行 fsync 操作。
  • 這個段被打開,使其可被搜索。
  • 內存緩衝區被清空。

3、這個進程繼續工作,更多的文檔被添加到內存緩衝區和追加到事務日誌
在這裏插入圖片描述
4、每隔一段時間–例如 translog 變得越來越大–索引被刷新(flush);一個新的 translog 被創建,並且一個全量提交被執行
在這裏插入圖片描述

  • 所有在內存緩衝區的文檔都被寫入一個新的段。
  • 緩衝區被清空。
  • 一個提交點被寫入硬盤。
  • 文件系統緩存通過 fsync 被刷新(flush)。
  • 老的 translog 被刪除。

Segment在被refresh之前,數據保存在內存中,是不可被搜索的,這也就是爲什麼 Lucene 被稱爲提供近實時而非實時查詢的原因。

但是如上這種機制避免了隨機寫,數據寫入都是 Batch 和 Append,能達到很高的吞吐量。同時爲了提高寫入的效率,利用了文件緩存系統和內存來加速寫入時的性能,並使用日誌來防止數據的丟失。

對比LSM樹
LSM-Tree 示意圖如下,可見 Lucene 的寫入思想和 LSM-Tree 是一致的:
在這裏插入圖片描述

三、Segment段維度

倒排索引

終於說到倒排索引了,都說倒排索引提升了搜索的速度,那麼具體採用了哪些架構或者數據結構來達成這一目標?
在這裏插入圖片描述
如上是Lucene中實際的索引結構。用例子來說明上述三個概念:在這裏插入圖片描述
ID是文檔id,那麼建立的索引如下:

Name:在這裏插入圖片描述
Age:
在這裏插入圖片描述
Sex:
在這裏插入圖片描述
Posting List
可見爲每個 field 都建立了一個倒排索引。Posting list就是一個int的數組,存儲了所有符合某個term的文檔id。實際上,除此之外還包含:文檔的數量、詞條在每個文檔中出現的次數、出現的位置、每個文檔的長度、所有文檔的平均長度等,在計算相關度時使用。

Term Dictionary

假設我們有很多個 term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照這樣的順序排列,找出某個特定的 term 一定很慢,因爲 term 沒有排序,需要全部過濾一遍才能找出特定的 term。排序之後就變成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

這樣我們可以用二分查找的方式,比全遍歷更快地找出目標的 term。這個就是 term dictionary。有了 term dictionary 之後,可以用 logN 次磁盤查找得到目標。

Term Index

但是磁盤的隨機讀操作仍然是非常昂貴的(一次 random access 大概需要 10ms 的時間)。所以儘量少的讀磁盤,有必要把一些數據緩存到內存裏。但是整個 term dictionary 本身又太大了,無法完整地放到內存裏。於是就有了 term index。term index 有點像一本字典的大的章節表。比如:

A 開頭的 term …………… Xxx 頁
C 開頭的 term …………… Yyy 頁
E 開頭的 term …………… Zzz 頁

如果所有的 term 都是英文字符的話,可能這個 term index 就真的是 26 個英文字符表構成的了。但是實際的情況是,term 未必都是英文字符,term 可以是任意的 byte 數組。而且 26 個英文字符也未必是每一個字符都有均等的 term,比如 x 字符開頭的 term 可能一個都沒有,而 s 開頭的 term 又特別多。實際的 term index 是一棵 trie 樹:
在這裏插入圖片描述
例子是一個包含 “A”, “to”, “tea”, “ted”, “ten”, “i”, “in”, 和 “inn” 的 trie 樹。這棵樹不會包含所有的 term,它包含的是 term 的一些前綴。通過 term index 可以快速地定位到 term dictionary 的某個 offset,然後從這個位置再往後順序查找。

現在我們可以回答“爲什麼 Elasticsearch/Lucene 檢索可以比 mysql 快了。Mysql 只有 term dictionary 這一層,是以 b-tree 排序的方式存儲在磁盤上的。檢索一個 term 需要若干次的 random access 的磁盤操作。而 Lucene 在 term dictionary 的基礎上添加了 term index 來加速檢索,term index 以樹的形式緩存在內存中。從 term index 查到對應的 term dictionary 的 block 位置之後,再去磁盤上找 term,大大減少了磁盤的 random access 次數。

FST(finite-state transducer)

實際上,Lucene 內部的 Term Index 是用的【變種的】trie樹,即 FST 。
FST 比 trie樹好在哪?trie樹只共享了前綴,而 FST 既共享前綴也共享後綴,更加的節省空間。

共享前綴與共享後綴的區別,例如,有下面兩個Term:

apple
dobble
dog

trie樹共享前綴:
在這裏插入圖片描述
FST既共享前綴也共享後綴:FST樹生成網站
在這裏插入圖片描述

一個FST是一個6元組 (Q, I, O, S, E, f):
Q是一個有限的狀態集
I是一個有限的輸入符號集
O是一個有限的輸出符號集
S是Q中的一個狀態,稱爲初始狀態
E是Q的一個子集,稱爲終止狀態集
f是轉換函數, f ⊆ Q × (I∪{ε}) × (O∪{ε}) × Q,其中ε表示空字符。 即從一個狀態q1開始,接收一個輸入字符i,可以到達另一個狀態q2,併產生輸出o。
例如有下面一組映射關係:

cat -> 5
deep -> 10
do -> 15
dog -> 2
dogs -> 8

可以用下圖中的FST來表示:
在這裏插入圖片描述
想想爲啥不用 HashMap,HashMap 也能實現有序Map?耗內存啊!
犧牲了一點性能來節約內存,旨在把所有Term Index都放在內存裏面,最終的效果是提升了速度。如上可知,FST是壓縮字典樹後綴的圖結構,他擁有Trie高效搜索能力,同時還非常小。這樣的話我們的搜索時,能把整個FST加載到內存。

總結一下,FST有更高的數據壓縮率和查詢效率,因爲詞典是常駐內存的,而 FST 有很好的壓縮率,所以 FST 在 Lucene 的最新版本中有非常多的使用場景,也是默認的詞典數據結構。

詞典的完整結構

Lucene 的tip文件即爲 Term Index 結構,tim文件即爲 Term Dictionary 結構。由圖可視,tip中存儲的就是多個FST, FST中存儲的是<單詞前綴,以該前綴開頭的所有Term的壓縮塊在磁盤中的位置>。即爲前文提到的從 term index 查到對應的 term dictionary 的 block 位置之後,再去磁盤上找 term,大大減少了磁盤的 random access 次數。
在這裏插入圖片描述
可以形象地理解爲,Term Dictionary 就是新華字典的正文部分包含了所有的詞彙,Term Index 就是新華字典前面的索引頁,用於表明詞彙在哪一頁。

但是 FST 即不能知道某個Term在Dictionary(.tim)文件上具體的位置,也不能僅通過FST就能確切的知道Term是否真實存在。它只能告訴你,查詢的Term可能在這些Blocks上,到底存不存在FST並不能給出確切的答案,因爲FST是通過Dictionary的每個Block的前綴構成,所以通過FST只可以直接找到這個Block在.tim文件上具體的File Pointer,並無法直接找到Terms。

四、如何聯合索引查詢?

回到上面的例子,給定查詢過濾條件 age=24 的過程就是:先從 term index 找到 24 在 term dictionary 的大概位置,然後再從 term dictionary 裏精確地找到 24 這個 term,然後得到一個 posting list 或者一個指向 posting list 位置的指針。然後再查詢 sex=Female 的過程也是類似的。最後得出 age= 24 AND sex=Female 就是把兩個 posting list 做一個“與”的合併。

這個理論上的“與”合併的操作可不容易。對於 mysql 來說,如果你給 age 和 gender 兩個字段都建立了索引,查詢的時候只會選擇其中最 selective 的來用,然後另外一個條件是在遍歷行的過程中在內存中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:

  • 使用 skip list 數據結構。同時遍歷 gender 和 age 的 posting list,互相 skip;
  • 使用 bitset 數據結構,對 gender 和 age 兩個 filter 分別求出 bitset,對兩個 bitset 做 AN 操作。

Elasticsearch 支持以上兩種的聯合索引方式,如果查詢的 filter 緩存到了內存中(以 bitset 的形式),那麼合併就是兩個 bitset 的 AND。如果查詢的 filter 沒有緩存,那麼就用 skip list 的方式去遍歷兩個 on disk 的 posting list。

參考:
Elasticsearch爲什麼這麼快

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