一文吃透Elasticsearch

程式設計師大彬發表於2023-04-21

本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~

Github地址

如果訪問不了Github,可以訪問gitee地址。

gitee地址


跟大家分享Elasticsearch的基礎知識,它是做什麼的以及它的使用和基本原理。

一、生活中的資料

搜尋引擎是對資料的檢索,所以我們先從生活中的資料說起。我們生活中的資料總體分為兩種:

  • 結構化資料
  • 非結構化資料

結構化資料: 也稱作行資料,是由二維表結構來邏輯表達和實現的資料,嚴格地遵循資料格式與長度規範,主要透過關係型資料庫進行儲存和管理。指具有固定格式或有限長度的資料,如資料庫,後設資料等。

非結構化資料: 又可稱為全文資料,不定長或無固定格式,不適於由資料庫二維表來表現,包括所有格式的辦公文件、XML、HTML、Word 文件,郵件,各類報表、圖片和咅頻、影片資訊等。

說明:如果要更細緻的區分的話,XML、HTML 可劃分為半結構化資料。因為它們也具有自己特定的標籤格式,所以既可以根據需要按結構化資料來處理,也可抽取出純文字按非結構化資料來處理。最全面的Java面試網站

根據兩種資料分類,搜尋也相應的分為兩種:

  • 結構化資料搜尋
  • 非結構化資料搜尋

對於結構化資料,因為它們具有特定的結構,所以我們一般都是可以透過關係型資料庫(MySQL,Oracle 等)的二維表(Table)的方式儲存和搜尋,也可以建立索引。

對於非結構化資料,也即對全文資料的搜尋主要有兩種方法:

  • 順序掃描
  • 全文檢索

順序掃描: 透過文字名稱也可瞭解到它的大概搜尋方式,即按照順序掃描的方式查詢特定的關鍵字。

例如給你一張報紙,讓你找到該報紙中“平安”的文字在哪些地方出現過。你肯定需要從頭到尾把報紙閱讀掃描一遍然後標記出關鍵字在哪些版塊出現過以及它的出現位置。

這種方式無疑是最耗時的最低效的,如果報紙排版字型小,而且版塊較多甚至有多份報紙,等你掃描完你的眼睛也差不多了。

全文搜尋: 對非結構化資料順序掃描很慢,我們是否可以進行最佳化?把我們的非結構化資料想辦法弄得有一定結構不就行了嗎?

將非結構化資料中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對此有一定結構的資料進行搜尋,從而達到搜尋相對較快的目的。

這種方式就構成了全文檢索的基本思路。這部分從非結構化資料中提取出的然後重新組織的資訊,我們稱之為索引。

這種方式的主要工作量在前期索引的建立,但是對於後期搜尋卻是快速高效的。

二、先說說 Lucene

透過對生活中資料的型別作了一個簡短了解之後,我們知道關係型資料庫的 SQL 檢索是處理不了這種非結構化資料的。

這種非結構化資料的處理需要依賴全文搜尋,而目前市場上開放原始碼的最好全文檢索引擎工具包就屬於 Apache 的 Lucene了。

但是 Lucene 只是一個工具包,它不是一個完整的全文檢索引擎。Lucene 的目的是為軟體開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此為基礎建立起完整的全文檢索引擎。

目前以 Lucene 為基礎建立的開源可用全文搜尋引擎主要是 Solr 和 Elasticsearch。

Solr 和 Elasticsearch 都是比較成熟的全文搜尋引擎,能完成的功能和效能也基本一樣。

但是 ES 本身就具有分散式的特性和易安裝使用的特點,而 Solr 的分散式需要藉助第三方來實現,例如透過使用 ZooKeeper 來達到分散式協調管理。

不管是 Solr 還是 Elasticsearch 底層都是依賴於 Lucene,而 Lucene 能實現全文搜尋主要是因為它實現了倒排索引的查詢結構。

如何理解倒排索引呢? 假如現有三份資料文件,文件的內容如下分別是:

  • Java is the best programming language.
  • PHP is the best programming language.
  • Javascript is the best programming language.

為了建立倒排索引,我們透過分詞器將每個文件的內容域拆分成單獨的詞(我們稱它為詞條或 Term),建立一個包含所有不重複詞條的排序列表,然後列出每個詞條出現在哪個文件。

結果如下所示:

Term          Doc_1    Doc_2   Doc_3  
-------------------------------------  
Java        |   X   |        |  
is          |   X   |   X    |   X  
the         |   X   |   X    |   X  
best        |   X   |   X    |   X  
programming |   x   |   X    |   X  
language    |   X   |   X    |   X  
PHP         |       |   X    |  
Javascript  |       |        |   X  
-------------------------------------  

這種結構由文件中所有不重複詞的列表構成,對於其中每個詞都有一個文件列表與之關聯。

這種由屬性值來確定記錄的位置的結構就是倒排索引。帶有倒排索引的檔案我們稱為倒排檔案。

我們將上面的內容轉換為圖的形式來說明倒排索引的結構資訊,如下圖所示:

其中主要有如下幾個核心術語需要理解:

  • 詞條(Term): 索引裡面最小的儲存和查詢單元,對於英文來說是一個單詞,對於中文來說一般指分詞後的一個詞。
  • 詞典(Term Dictionary): 或字典,是詞條 Term 的集合。搜尋引擎的通常索引單位是單詞,單詞詞典是由文件集合中出現過的所有單詞構成的字串集合,單詞詞典內每條索引項記載單詞本身的一些資訊以及指向“倒排列表”的指標。
  • 倒排表(Post list): 一個文件通常由多個片語成,倒排表記錄的是某個詞在哪些文件裡出現過以及出現的位置。每條記錄稱為一個倒排項(Posting)。倒排表記錄的不單是文件編號,還儲存了詞頻等資訊。
  • 倒排檔案(Inverted File): 所有單詞的倒排列表往往順序地儲存在磁碟的某個檔案裡,這個檔案被稱之為倒排檔案,倒排檔案是儲存倒排索引的物理檔案。

從上圖我們可以瞭解到倒排索引主要由兩個部分組成:

  • 詞典
  • 倒排檔案

詞典和倒排表是 Lucene 中很重要的兩種資料結構,是實現快速檢索的重要基石。詞典和倒排檔案是分兩部分儲存的,詞典在記憶體中而倒排檔案儲存在磁碟上。

給大家分享一個Github倉庫,上面有大彬整理的300多本經典的計算機書籍PDF,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~

Github地址

三、ES 核心概念

一些基礎知識的鋪墊之後我們正式進入今天的主角 Elasticsearch 的介紹。

ES 是使用 Java 編寫的一種開源搜尋引擎,它在內部使用 Lucene 做索引與搜尋,透過對 Lucene 的封裝,隱藏了 Lucene 的複雜性,取而代之的提供一套簡單一致的 RESTful API。

然而,Elasticsearch 不僅僅是 Lucene,並且也不僅僅只是一個全文搜尋引擎。

它可以被下面這樣準確的形容:

  • 一個分散式的實時文件儲存,每個欄位可以被索引與搜尋。
  • 一個分散式實時分析搜尋引擎。
  • 能勝任上百個服務節點的擴充套件,並支援 PB 級別的結構化或者非結構化資料。

官網對 Elasticsearch 的介紹是 Elasticsearch 是一個分散式、可擴充套件、近實時的搜尋與資料分析引擎。

我們透過一些核心概念來看下 Elasticsearch 是如何做到分散式,可擴充套件和近實時搜尋的。

叢集(Cluster)

ES 的叢集搭建很簡單,不需要依賴第三方協調管理元件,自身內部就實現了叢集的管理功能。

ES 叢集由一個或多個 Elasticsearch 節點組成,每個節點配置相同的 cluster.name 即可加入叢集,預設值為 “elasticsearch”。

確保不同的環境中使用不同的叢集名稱,否則最終會導致節點加入錯誤的叢集。

一個 Elasticsearch 服務啟動例項就是一個節點(Node)。節點透過 node.name 來設定節點名稱,如果不設定則在啟動時給節點分配一個隨機通用唯一識別符號作為名稱。

①發現機制

那麼有一個問題,ES 內部是如何透過一個相同的設定 cluster.name 就能將不同的節點連線到同一個叢集的?答案是 Zen Discovery。

Zen Discovery 是 Elasticsearch 的內建預設發現模組(發現模組的職責是發現叢集中的節點以及選舉 Master 節點)。

它提供單播和基於檔案的發現,並且可以擴充套件為透過外掛支援雲環境和其他形式的發現。

Zen Discovery 與其他模組整合,例如,節點之間的所有通訊都使用 Transport 模組完成。節點使用發現機制透過 Ping 的方式查詢其他節點。

Elasticsearch 預設被配置為使用單播發現,以防止節點無意中加入叢集。只有在同一臺機器上執行的節點才會自動組成叢集。

如果叢集的節點執行在不同的機器上,使用單播,你可以為 Elasticsearch 提供一些它應該去嘗試連線的節點列表。

當一個節點聯絡到單播列表中的成員時,它就會得到整個叢集所有節點的狀態,然後它會聯絡 Master 節點,並加入叢集。

這意味著單播列表不需要包含叢集中的所有節點, 它只是需要足夠的節點,當一個新節點聯絡上其中一個並且說上話就可以了。

如果你使用 Master 候選節點作為單播列表,你只要列出三個就可以了。這個配置在 elasticsearch.yml 檔案中:

discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]  

節點啟動後先 Ping ,如果 discovery.zen.ping.unicast.hosts 有設定,則 Ping 設定中的 Host ,否則嘗試 ping localhost 的幾個埠。

Elasticsearch 支援同一個主機啟動多個節點,Ping 的 Response 會包含該節點的基本資訊以及該節點認為的 Master 節點。

選舉開始,先從各節點認為的 Master 中選,規則很簡單,按照 ID 的字典序排序,取第一個。如果各節點都沒有認為的 Master ,則從所有節點中選擇,規則同上。

這裡有個限制條件就是 discovery.zen.minimum_master_nodes ,如果節點數達不到最小值的限制,則迴圈上述過程,直到節點數足夠可以開始選舉。

最後選舉結果是肯定能選舉出一個 Master ,如果只有一個 Local 節點那就選出的是自己。

如果當前節點是 Master ,則開始等待節點數達到 discovery.zen.minimum_master_nodes,然後提供服務。

如果當前節點不是 Master ,則嘗試加入 Master 。Elasticsearch 將以上服務發現以及選主的流程叫做 Zen Discovery 。

由於它支援任意數目的叢集( 1- N ),所以不能像 Zookeeper 那樣限制節點必須是奇數,也就無法用投票的機制來選主,而是透過一個規則。

只要所有的節點都遵循同樣的規則,得到的資訊都是對等的,選出來的主節點肯定是一致的。

但分散式系統的問題就出在資訊不對等的情況,這時候很容易出現腦裂(Split-Brain)的問題。

大多數解決方案就是設定一個 Quorum 值,要求可用節點必須大於 Quorum(一般是超過半數節點),才能對外提供服務。

而 Elasticsearch 中,這個 Quorum 的配置就是 discovery.zen.minimum_master_nodes

②節點的角色

每個節點既可以是候選主節點也可以是資料節點,透過在配置檔案 ../config/elasticsearch.yml 中設定即可,預設都為 true。

node.master: true  //是否候選主節點  
node.data: true    //是否資料節點  

資料節點負責資料的儲存和相關的操作,例如對資料進行增、刪、改、查和聚合等操作,所以資料節點(Data 節點)對機器配置要求比較高,對 CPU、記憶體和 I/O 的消耗很大。

通常隨著叢集的擴大,需要增加更多的資料節點來提高效能和可用性。

候選主節點可以被選舉為主節點(Master 節點),叢集中只有候選主節點才有選舉權和被選舉權,其他節點不參與選舉的工作。

主節點負責建立索引、刪除索引、跟蹤哪些節點是群集的一部分,並決定哪些分片分配給相關的節點、追蹤叢集中節點的狀態等,穩定的主節點對叢集的健康是非常重要的。

一個節點既可以是候選主節點也可以是資料節點,但是由於資料節點對 CPU、記憶體核 I/O 消耗都很大。

所以如果某個節點既是資料節點又是主節點,那麼可能會對主節點產生影響從而對整個叢集的狀態產生影響。

因此為了提高叢集的健康性,我們應該對 Elasticsearch 叢集中的節點做好角色上的劃分和隔離。可以使用幾個配置較低的機器群作為候選主節點群。

主節點和其他節點之間透過 Ping 的方式互檢查,主節點負責 Ping 所有其他節點,判斷是否有節點已經掛掉。其他節點也透過 Ping 的方式判斷主節點是否處於可用狀態。

雖然對節點做了角色區分,但是使用者的請求可以發往任何一個節點,並由該節點負責分發請求、收集結果等操作,而不需要主節點轉發。

這種節點可稱之為協調節點,協調節點是不需要指定和配置的,叢集中的任何節點都可以充當協調節點的角色。

③腦裂現象

同時如果由於網路或其他原因導致叢集中選舉出多個 Master 節點,使得資料更新時出現不一致,這種現象稱之為腦裂,即叢集中不同的節點對於 Master 的選擇出現了分歧,出現了多個 Master 競爭。

“腦裂”問題可能有以下幾個原因造成:

  • 網路問題: 叢集間的網路延遲導致一些節點訪問不到 Master,認為 Master 掛掉了從而選舉出新的 Master,並對 Master 上的分片和副本標紅,分配新的主分片。
  • 節點負載: 主節點的角色既為 Master 又為 Data,訪問量較大時可能會導致 ES 停止響應(假死狀態)造成大面積延遲,此時其他節點得不到主節點的響應認為主節點掛掉了,會重新選取主節點。
  • 記憶體回收: 主節點的角色既為 Master 又為 Data,當 Data 節點上的 ES 程式佔用的記憶體較大,引發 JVM 的大規模記憶體回收,造成 ES 程式失去響應。

為了避免腦裂現象的發生,我們可以從原因著手透過以下幾個方面來做出最佳化措施:

  • 適當調大響應時間,減少誤判。 透過引數 discovery.zen.ping_timeout 設定節點狀態的響應時間,預設為 3s,可以適當調大。

如果 Master 在該響應時間的範圍內沒有做出響應應答,判斷該節點已經掛掉了。調大引數(如 6s,discovery.zen.ping_timeout:6),可適當減少誤判。

  • 選舉觸發。 我們需要在候選叢集中的節點的配置檔案中設定引數 discovery.zen.munimum_master_nodes 的值。

這個參數列示在選舉主節點時需要參與選舉的候選主節點的節點數,預設值是 1,官方建議取值(master_eligibel_nodes2)+1,其中 master_eligibel_nodes 為候選主節點的個數。

這樣做既能防止腦裂現象的發生,也能最大限度地提升叢集的高可用性,因為只要不少於 discovery.zen.munimum_master_nodes 個候選節點存活,選舉工作就能正常進行。

當小於這個值的時候,無法觸發選舉行為,叢集無法使用,不會造成分片混亂的情況。

  • 角色分離。 即是上面我們提到的候選主節點和資料節點進行角色分離,這樣可以減輕主節點的負擔,防止主節點的假死狀態發生,減少對主節點“已死”的誤判。

分片(Shards)

ES 支援 PB 級全文搜尋,當索引上的資料量太大的時候,ES 透過水平拆分的方式將一個索引上的資料拆分出來分配到不同的資料塊上,拆分出來的資料庫塊稱之為一個分片。

這類似於 MySQL 的分庫分表,只不過 MySQL 分庫分表需要藉助第三方元件而 ES 內部自身實現了此功能。

在一個多分片的索引中寫入資料時,透過路由來確定具體寫入哪一個分片中,所以在建立索引的時候需要指定分片的數量,並且分片的數量一旦確定就不能修改。

分片的數量和下面介紹的副本數量都是可以透過建立索引時的 Settings 來配置,ES 預設為一個索引建立 5 個主分片, 並分別為每個分片建立一個副本。

PUT /myIndex  
{  
   "settings" : {  
      "number_of_shards" : 5,  
      "number_of_replicas" : 1  
   }  
}  

ES 透過分片的功能使得索引在規模上和效能上都得到提升,每個分片都是 Lucene 中的一個索引檔案,每個分片必須有一個主分片和零到多個副本。

副本(Replicas)

副本就是對分片的 Copy,每個主分片都有一個或多個副本分片,當主分片異常時,副本可以提供資料的查詢等操作。

主分片和對應的副本分片是不會在同一個節點上的,所以副本分片數的最大值是 N-1(其中 N 為節點數)。

對文件的新建、索引和刪除請求都是寫操作,必須在主分片上面完成之後才能被複制到相關的副本分片。

ES 為了提高寫入的能力這個過程是併發寫的,同時為了解決併發寫的過程中資料衝突的問題,ES 透過樂觀鎖的方式控制,每個文件都有一個 _version (版本)號,當文件被修改時版本號遞增。

一旦所有的副本分片都報告寫成功才會向協調節點報告成功,協調節點向客戶端報告成功。

從上圖可以看出為了達到高可用,Master 節點會避免將主分片和副本分片放在同一個節點上。

假設這時節點 Node1 服務當機了或者網路不可用了,那麼主節點上主分片 S0 也就不可用了。

幸運的是還存在另外兩個節點能正常工作,這時 ES 會重新選舉新的主節點,而且這兩個節點上存在我們所需要的 S0 的所有資料。

我們會將 S0 的副本分片提升為主分片,這個提升主分片的過程是瞬間發生的。此時叢集的狀態將會為 Yellow。

為什麼我們叢集狀態是 Yellow 而不是 Green 呢?雖然我們擁有所有的 2 個主分片,但是同時設定了每個主分片需要對應兩份副本分片,而此時只存在一份副本分片。所以叢集不能為 Green 的狀態。

如果我們同樣關閉了 Node2 ,我們的程式依然可以保持在不丟失任何資料的情況下執行,因為 Node3 為每一個分片都保留著一份副本。

如果我們重新啟動 Node1 ,叢集可以將缺失的副本分片再次進行分配,那麼叢集的狀態又將恢復到原來的正常狀態。

如果 Node1 依然擁有著之前的分片,它將嘗試去重用它們,只不過這時 Node1 節點上的分片不再是主分片而是副本分片了,如果期間有更改的資料只需要從主分片上覆制修改的資料檔案即可。

小結:

  • 將資料分片是為了提高可處理資料的容量和易於進行水平擴充套件,為分片做副本是為了提高叢集的穩定性和提高併發量。
  • 副本是乘法,越多消耗越大,但也越保險。分片是除法,分片越多,單分片資料就越少也越分散。
  • 副本越多,叢集的可用性就越高,但是由於每個分片都相當於一個 Lucene 的索引檔案,會佔用一定的檔案控制程式碼、記憶體及 CPU。並且分片間的資料同步也會佔用一定的網路頻寬,所以索引的分片數和副本數也不是越多越好。

對映(Mapping)

對映是用於定義 ES 對索引中欄位的儲存型別、分詞方式和是否儲存等資訊,就像資料庫中的 Schema ,描述了文件可能具有的欄位或屬性、每個欄位的資料型別。

只不過關係型資料庫建表時必須指定欄位型別,而 ES 對於欄位型別可以不指定然後動態對欄位型別猜測,也可以在建立索引時具體指定欄位的型別。

對欄位型別根據資料格式自動識別的對映稱之為動態對映(Dynamic Mapping),我們建立索引時具體定義欄位型別的對映稱之為靜態對映或顯示對映(Explicit Mapping)。

在講解動態對映和靜態對映的使用前,我們先來了解下 ES 中的資料有哪些欄位型別?之後我們再講解為什麼我們建立索引時需要建立靜態對映而不使用動態對映。

ES(v6.8)中欄位資料型別主要有以下幾類:

Text 用於索引全文值的欄位,例如電子郵件正文或產品說明。這些欄位是被分詞的,它們透過分詞器傳遞 ,以在被索引之前將字串轉換為單個術語的列表。

分析過程允許 Elasticsearch 搜尋單個單詞中每個完整的文字欄位。文字欄位不用於排序,很少用於聚合。

Keyword 用於索引結構化內容的欄位,例如電子郵件地址,主機名,狀態程式碼,郵政編碼或標籤。它們通常用於過濾,排序,和聚合。Keyword 欄位只能按其確切值進行搜尋。

透過對欄位型別的瞭解我們知道有些欄位需要明確定義的,例如某個欄位是 Text 型別還是 Keyword 型別差別是很大的,時間欄位也許我們需要指定它的時間格式,還有一些欄位我們需要指定特定的分詞器等等。

如果採用動態對映是不能精確做到這些的,自動識別常常會與我們期望的有些差異。

所以建立索引的時候一個完整的格式應該是指定分片和副本數以及 Mapping 的定義,如下:

PUT my_index   
{  
   "settings" : {  
      "number_of_shards" : 5,  
      "number_of_replicas" : 1  
   }  
  "mappings": {  
    "_doc": {   
      "properties": {   
        "title":    { "type": "text"  },   
        "name":     { "type": "text"  },   
        "age":      { "type": "integer" },    
        "created":  {  
          "type":   "date",   
          "format": "strict_date_optional_time||epoch_millis"  
        }  
      }  
    }  
  }  
}  

四、ES 的基本使用

在決定使用 Elasticsearch 的時候首先要考慮的是版本問題,Elasticsearch (排除 0.x 和 1.x)目前有如下常用的穩定的主版本:2.x,5.x,6.x,7.x(current)。

你可能會發現沒有 3.x 和 4.x,ES 從 2.4.6 直接跳到了 5.0.0。其實是為了 ELK(ElasticSearch,Logstash,Kibana)技術棧的版本統一,免的給使用者帶來混亂。

在 Elasticsearch 是 2.x (2.x 的最後一版 2.4.6 的釋出時間是 July 25, 2017) 的情況下,Kibana 已經是 4.x(Kibana 4.6.5 的釋出時間是 July 25, 2017)。

那麼在 Kibana 的下一主版本肯定是 5.x 了,所以 Elasticsearch 直接將自己的主版本釋出為 5.0.0 了。

統一之後,我們選版本就不會猶豫困惑了,我們選定 Elasticsearch 的版本後再選擇相同版本的 Kibana 就行了,不用擔憂版本不相容的問題。

Elasticsearch 是使用 Java 構建,所以除了注意 ELK 技術的版本統一,我們在選擇 Elasticsearch 的版本的時候還需要注意 JDK 的版本。

因為每個大版本所依賴的 JDK 版本也不同,目前 7.2 版本已經可以支援 JDK11。

安裝使用

①下載和解壓 Elasticsearch,無需安裝解壓後即可用,解壓後目錄:

  • bin:二進位制系統指令目錄,包含啟動命令和安裝外掛命令等。
  • config:配置檔案目錄。
  • data:資料儲存目錄。
  • lib:依賴包目錄。
  • logs:日誌檔案目錄。
  • modules:模組庫,例如 x-pack 的模組。
  • plugins:外掛目錄。

②安裝目錄下執行 bin/elasticsearch 來啟動 ES。

③預設在 9200 埠執行,請求 curl http://localhost:9200/ 或者瀏覽器輸入 http://localhost:9200,得到一個 JSON 物件,其中包含當前節點、叢集、版本等資訊。

{  
  "name" : "U7fp3O9",  
  "cluster_name" : "elasticsearch",  
  "cluster_uuid" : "-Rj8jGQvRIelGd9ckicUOA",  
  "version" : {  
    "number" : "6.8.1",  
    "build_flavor" : "default",  
    "build_type" : "zip",  
    "build_hash" : "1fad4e1",  
    "build_date" : "2019-06-18T13:16:52.517138Z",  
    "build_snapshot" : false,  
    "lucene_version" : "7.7.0",  
    "minimum_wire_compatibility_version" : "5.6.0",  
    "minimum_index_compatibility_version" : "5.0.0"  
  },  
  "tagline" : "You Know, for Search"  
}  

叢集健康狀態

要檢查群集執行狀況,我們可以在 Kibana 控制檯中執行以下命令 GET /_cluster/health,得到如下資訊:

{  
  "cluster_name" : "wujiajian",  
  "status" : "yellow",  
  "timed_out" : false,  
  "number_of_nodes" : 1,  
  "number_of_data_nodes" : 1,  
  "active_primary_shards" : 9,  
  "active_shards" : 9,  
  "relocating_shards" : 0,  
  "initializing_shards" : 0,  
  "unassigned_shards" : 5,  
  "delayed_unassigned_shards" : 0,  
  "number_of_pending_tasks" : 0,  
  "number_of_in_flight_fetch" : 0,  
  "task_max_waiting_in_queue_millis" : 0,  
  "active_shards_percent_as_number" : 64.28571428571429  
}  

叢集狀態透過 綠,黃,紅 來標識:

  • 綠色:叢集健康完好,一切功能齊全正常,所有分片和副本都可以正常工作。
  • 黃色:預警狀態,所有主分片功能正常,但至少有一個副本是不能正常工作的。此時叢集是可以正常工作的,但是高可用性在某種程度上會受影響。
  • 紅色:叢集不可正常使用。某個或某些分片及其副本異常不可用,這時叢集的查詢操作還能執行,但是返回的結果會不準確。對於分配到這個分片的寫入請求將會報錯,最終會導致資料的丟失。

當叢集狀態為紅色時,它將會繼續從可用的分片提供搜尋請求服務,但是你需要儘快修復那些未分配的分片。

五、ES 機制原理

ES 的基本概念和基本操作介紹完了之後,我們可能還有很多疑惑:

  • 它們內部是如何執行的?
  • 主分片和副本分片是如何同步的?
  • 建立索引的流程是什麼樣的?
  • ES 如何將索引資料分配到不同的分片上的?以及這些索引資料是如何儲存的?
  • 為什麼說 ES 是近實時搜尋引擎而文件的 CRUD (建立-讀取-更新-刪除) 操作是實時的?
  • 以及 Elasticsearch 是怎樣保證更新被持久化在斷電時也不丟失資料?
  • 還有為什麼刪除文件不會立刻釋放空間?

帶著這些疑問我們進入接下來的內容。

寫索引原理

下圖描述了 3 個節點的叢集,共擁有 12 個分片,其中有 4 個主分片(S0、S1、S2、S3)和 8 個副本分片(R0、R1、R2、R3),每個主分片對應兩個副本分片,節點 1 是主節點(Master 節點)負責整個叢集的狀態。

寫索引是隻能寫在主分片上,然後同步到副本分片。這裡有四個主分片,一條資料 ES 是根據什麼規則寫到特定分片上的呢?

這條索引資料為什麼被寫到 S0 上而不寫到 S1 或 S2 上?那條資料為什麼又被寫到 S3 上而不寫到 S0 上了?

首先這肯定不會是隨機的,否則將來要獲取文件的時候我們就不知道從何處尋找了。

實際上,這個過程是根據下面這個公式決定的:

shard = hash(routing) % number_of_primary_shards  

Routing 是一個可變值,預設是文件的 _id ,也可以設定成一個自定義的值。

Routing 透過 Hash 函式生成一個數字,然後這個數字再除以 number_of_primary_shards (主分片的數量)後得到餘數。

這個在 0 到 number_of_primary_shards-1 之間的餘數,就是我們所尋求的文件所在分片的位置。

這就解釋了為什麼我們要在建立索引的時候就確定好主分片的數量並且永遠不會改變這個數量:因為如果數量變化了,那麼所有之前路由的值都會無效,文件也再也找不到了。

由於在 ES 叢集中每個節點透過上面的計算公式都知道叢集中的文件的存放位置,所以每個節點都有處理讀寫請求的能力。

在一個寫請求被髮送到某個節點後,該節點即為前面說過的協調節點,協調節點會根據路由公式計算出需要寫到哪個分片上,再將請求轉發到該分片的主分片節點上。

假如此時資料透過路由計算公式取餘後得到的值是 shard=hash(routing)%4=0

則具體流程如下:

  • 客戶端向 ES1 節點(協調節點)傳送寫請求,透過路由計算公式得到值為 0,則當前資料應被寫到主分片 S0 上。
  • ES1 節點將請求轉發到 S0 主分片所在的節點 ES3,ES3 接受請求並寫入到磁碟。
  • 併發將資料複製到兩個副本分片 R0 上,其中透過樂觀併發控制資料的衝突。一旦所有的副本分片都報告成功,則節點 ES3 將向協調節點報告成功,協調節點向客戶端報告成功。

儲存原理

上面介紹了在 ES 內部索引的寫處理流程,這個流程是在 ES 的記憶體中執行的,資料被分配到特定的分片和副本上之後,最終是儲存到磁碟上的,這樣在斷電的時候就不會丟失資料。

具體的儲存路徑可在配置檔案 ../config/elasticsearch.yml 中進行設定,預設儲存在安裝目錄的 Data 資料夾下。

建議不要使用預設值,因為若 ES 進行了升級,則有可能導致資料全部丟失:

path.data: /path/to/data  //索引資料  
path.logs: /path/to/logs  //日誌記錄  

①分段儲存

索引文件以段的形式儲存在磁碟上,何為段?索引檔案被拆分為多個子檔案,則每個子檔案叫作段,每一個段本身都是一個倒排索引,並且段具有不變性,一旦索引的資料被寫入硬碟,就不可再修改。

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

段被寫入到磁碟後會生成一個提交點,提交點是一個用來記錄所有提交後段資訊的檔案。

一個段一旦擁有了提交點,就說明這個段只有讀的許可權,失去了寫的許可權。相反,當段在記憶體中時,就只有寫的許可權,而不具備讀資料的許可權,意味著不能被檢索。

段的概念提出主要是因為:在早期全文檢索中為整個文件集合建立了一個很大的倒排索引,並將其寫入磁碟中。

如果索引有更新,就需要重新全量建立一個索引來替換原來的索引。這種方式在資料量很大時效率很低,並且由於建立一次索引的成本很高,所以對資料的更新不能過於頻繁,也就不能保證時效性。

索引檔案分段儲存並且不可修改,那麼新增、更新和刪除如何處理呢?

  • 新增,新增很好處理,由於資料是新的,所以只需要對當前文件新增一個段就可以了。
  • 刪除,由於不可修改,所以對於刪除操作,不會把文件從舊的段中移除而是透過新增一個 .del 檔案,檔案中會列出這些被刪除文件的段資訊。這個被標記刪除的文件仍然可以被查詢匹配到, 但它會在最終結果被返回前從結果集中移除。
  • 更新,不能修改舊的段來進行反映文件的更新,其實更新相當於是刪除和新增這兩個動作組成。會將舊的文件在 .del 檔案中標記刪除,然後文件的新版本被索引到一個新的段中。可能兩個版本的文件都會被一個查詢匹配到,但被刪除的那個舊版本文件在結果集返回前就會被移除。

段被設定為不可修改具有一定的優勢也有一定的缺點,優勢主要表現在:

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

段的不變性的缺點如下:

  • 當對舊資料進行刪除時,舊資料不會馬上被刪除,而是在 .del 檔案中被標記為刪除。而舊資料只能等到段更新時才能被移除,這樣會造成大量的空間浪費。
  • 若有一條資料頻繁的更新,每次更新都是新增新的標記舊的,則會有大量的空間浪費。
  • 每次新增資料時都需要新增一個段來儲存資料。當段的數量太多時,對伺服器的資源例如檔案控制程式碼的消耗會非常大。
  • 在查詢的結果中包含所有的結果集,需要排除被標記刪除的舊資料,這增加了查詢的負擔。

②延遲寫策略

介紹完了儲存的形式,那麼索引寫入到磁碟的過程是怎樣的?是否是直接調 Fsync 物理性地寫入磁碟?

答案是顯而易見的,如果是直接寫入到磁碟上,磁碟的 I/O 消耗上會嚴重影響效能。

那麼當寫資料量大的時候會造成 ES 停頓卡死,查詢也無法做到快速響應。如果真是這樣 ES 也就不會稱之為近實時全文搜尋引擎了。

為了提升寫的效能,ES 並沒有每新增一條資料就增加一個段到磁碟上,而是採用延遲寫的策略。

每當有新增的資料時,就將其先寫入到記憶體中,在記憶體和磁碟之間是檔案系統快取。

當達到預設的時間(1 秒鐘)或者記憶體的資料達到一定量時,會觸發一次重新整理(Refresh),將記憶體中的資料生成到一個新的段上並快取到檔案快取系統 上,稍後再被重新整理到磁碟中並生成提交點。

這裡的記憶體使用的是 ES 的 JVM 記憶體,而檔案快取系統使用的是作業系統的記憶體。

新的資料會繼續的被寫入記憶體,但記憶體中的資料並不是以段的形式儲存的,因此不能提供檢索功能。

由記憶體重新整理到檔案快取系統的時候會生成新的段,並將段開啟以供搜尋使用,而不需要等到被重新整理到磁碟。

在 Elasticsearch 中,寫入和開啟一個新段的輕量的過程叫做 Refresh (即記憶體重新整理到檔案快取系統)。

預設情況下每個分片會每秒自動重新整理一次。這就是為什麼我們說 Elasticsearch 是近實時搜尋,因為文件的變化並不是立即對搜尋可見,但會在一秒之內變為可見。

我們也可以手動觸發 Refresh,POST /_refresh 重新整理所有索引,POST /nba/_refresh 重新整理指定的索引。

Tips:儘管重新整理是比提交輕量很多的操作,它還是會有效能開銷。當寫測試的時候, 手動重新整理很有用,但是不要在生產>環境下每次索引一個文件都去手動重新整理。而且並不是所有的情況都需要每秒重新整理。

可能你正在使用 Elasticsearch 索引大量的日誌檔案, 你可能想最佳化索引速度而不是>近實時搜尋。

這時可以在建立索引時在 Settings 中透過調大 refresh_interval = "30s" 的值 , 降低每個索引的重新整理頻率,設值時需要注意後面帶上時間單位,否則預設是毫秒。當 refresh_interval=-1 時表示關閉索引的自動重新整理。

雖然透過延時寫的策略可以減少資料往磁碟上寫的次數提升了整體的寫入能力,但是我們知道檔案快取系統也是記憶體空間,屬於作業系統的記憶體,只要是記憶體都存在斷電或異常情況下丟失資料的危險。

為了避免丟失資料,Elasticsearch 新增了事務日誌(Translog),事務日誌記錄了所有還沒有持久化到磁碟的資料。

圖片

新增了事務日誌後整個寫索引的流程如上圖所示:

  • 一個新文件被索引之後,先被寫入到記憶體中,但是為了防止資料的丟失,會追加一份資料到事務日誌中。

    不斷有新的文件被寫入到記憶體,同時也都會記錄到事務日誌中。這時新資料還不能被檢索和查詢。

  • 當達到預設的重新整理時間或記憶體中的資料達到一定量後,會觸發一次 Refresh,將記憶體中的資料以一個新段形式重新整理到檔案快取系統中並清空記憶體。這時雖然新段未被提交到磁碟,但是可以提供文件的檢索功能且不能被修改。
  • 隨著新文件索引不斷被寫入,當日志資料大小超過 512M 或者時間超過 30 分鐘時,會觸發一次 Flush。

    記憶體中的資料被寫入到一個新段同時被寫入到檔案快取系統,檔案系統快取中資料透過 Fsync 重新整理到磁碟中,生成提交點,日誌檔案被刪除,建立一個空的新日誌。

透過這種方式當斷電或需要重啟時,ES 不僅要根據提交點去載入已經持久化過的段,還需要工具 Translog 裡的記錄,把未持久化的資料重新持久化到磁碟上,避免了資料丟失的可能。

③段合併

由於自動重新整理流程每秒會建立一個新的段 ,這樣會導致短時間內的段數量暴增。而段數目太多會帶來較大的麻煩。

每一個段都會消耗檔案控制程式碼、記憶體和 CPU 執行週期。更重要的是,每個搜尋請求都必須輪流檢查每個段然後合併查詢結果,所以段越多,搜尋也就越慢。

Elasticsearch 透過在後臺定期進行段合併來解決這個問題。小的段被合併到大的段,然後這些大的段再被合併到更大的段。

段合併的時候會將那些舊的已刪除文件從檔案系統中清除。被刪除的文件不會被複製到新的大段中。合併的過程中不會中斷索引和搜尋。

段合併在進行索引和搜尋時會自動進行,合併程式選擇一小部分大小相似的段,並且在後臺將它們合併到更大的段中,這些段既可以是未提交的也可以是已提交的。

合併結束後老的段會被刪除,新的段被 Flush 到磁碟,同時寫入一個包含新段且排除舊的和較小的段的新提交點,新的段被開啟可以用來搜尋。

段合併的計算量龐大, 而且還要吃掉大量磁碟 I/O,段合併會拖累寫入速率,如果任其發展會影響搜尋效能。

Elasticsearch 在預設情況下會對合並流程進行資源限制,所以搜尋仍然有足夠的資源很好地執行。

六、ES 的效能最佳化

儲存裝置

磁碟在現代伺服器上通常都是瓶頸。Elasticsearch 重度使用磁碟,你的磁碟能處理的吞吐量越大,你的節點就越穩定。

這裡有一些最佳化磁碟 I/O 的技巧:

  • 使用 SSD。就像其他地方提過的, 他們比機械磁碟優秀多了。
  • 使用 RAID 0。條帶化 RAID 會提高磁碟 I/O,代價顯然就是當一塊硬碟故障時整個就故障了。不要使用映象或者奇偶校驗 RAID 因為副本已經提供了這個功能。
  • 另外,使用多塊硬碟,並允許 Elasticsearch 透過多個 path.data 目錄配置把資料條帶化分配到它們上面。
  • 不要使用遠端掛載的儲存,比如 NFS 或者 SMB/CIFS。這個引入的延遲對效能來說完全是背道而馳的。
  • 如果你用的是 EC2,當心 EBS。即便是基於 SSD 的 EBS,通常也比本地例項的儲存要慢。

內部索引最佳化

Elasticsearch 為了能快速找到某個 Term,先將所有的 Term 排個序,然後根據二分法查詢 Term,時間複雜度為 logN,就像透過字典查詢一樣,這就是 Term Dictionary。

現在再看起來,似乎和傳統資料庫透過 B-Tree 的方式類似。但是如果 Term 太多,Term Dictionary 也會很大,放記憶體不現實,於是有了 Term Index。

就像字典裡的索引頁一樣,A 開頭的有哪些 Term,分別在哪頁,可以理解 Term Index是一棵樹。

這棵樹不會包含所有的 Term,它包含的是 Term 的一些字首。透過 Term Index 可以快速地定位到 Term Dictionary 的某個 Offset,然後從這個位置再往後順序查詢。

在記憶體中用 FST 方式壓縮 Term Index,FST 以位元組的方式儲存所有的 Term,這種壓縮方式可以有效的縮減儲存空間,使得 Term Index 足以放進記憶體,但這種方式也會導致查詢時需要更多的 CPU 資源。

對於儲存在磁碟上的倒排表同樣也採用了壓縮技術減少儲存所佔用的空間。

調整配置引數

調整配置引數建議如下:

  • 給每個文件指定有序的具有壓縮良好的序列模式 ID,避免隨機的 UUID-4 這樣的 ID,這樣的 ID 壓縮比很低,會明顯拖慢 Lucene。
  • 對於那些不需要聚合和排序的索引欄位禁用 Doc values。Doc Values 是有序的基於 document=>field value 的對映列表。
  • 不需要做模糊檢索的欄位使用 Keyword 型別代替 Text 型別,這樣可以避免在建立索引前對這些文字進行分詞。
  • 如果你的搜尋結果不需要近實時的準確度,考慮把每個索引的 index.refresh_interval 改到 30s 。

    如果你是在做大批次匯入,匯入期間你可以透過設定這個值為 -1 關掉重新整理,還可以透過設定 index.number_of_replicas: 0 關閉副本。別忘記在完工的時候重新開啟它。

  • 避免深度分頁查詢建議使用 Scroll 進行分頁查詢。普通分頁查詢時,會建立一個 from+size 的空優先佇列,每個分片會返回 from+size 條資料,預設只包含文件 ID 和得分 Score 給協調節點。

    如果有 N 個分片,則協調節點再對(from+size)×n 條資料進行二次排序,然後選擇需要被取回的文件。當 from 很大時,排序過程會變得很沉重,佔用 CPU 資源嚴重。

  • 減少對映欄位,只提供需要檢索,聚合或排序的欄位。其他欄位可存在其他儲存裝置上,例如 Hbase,在 ES 中得到結果後再去 Hbase 查詢這些欄位。
  • 建立索引和查詢時指定路由 Routing 值,這樣可以精確到具體的分片查詢,提升查詢效率。路由的選擇需要注意資料的分佈均衡。

JVM 調優

JVM 調優建議如下:

  • 確保堆記憶體最小值( Xms )與最大值( Xmx )的大小是相同的,防止程式在執行時改變堆記憶體大小。Elasticsearch 預設安裝後設定的堆記憶體是 1GB。可透過 ../config/jvm.option 檔案進行配置,但是最好不要超過實體記憶體的50%和超過 32GB。
  • GC 預設採用 CMS 的方式,併發但是有 STW 的問題,可以考慮使用 G1 收集器。
  • ES 非常依賴檔案系統快取(Filesystem Cache),快速搜尋。一般來說,應該至少確保物理上有一半的可用記憶體分配到檔案系統快取。

相關文章