還可以這麼理解 Elasticsearch

由於近期在公司內部做了一次 Elasticsearch 的分享,所以本篇主要是做一個總結,希望通過這篇文章能讓讀者大致瞭解 Elasticsearch 是做什麼的以及它的使用和基本原理。

生活中的數據

搜索引擎是對數據的檢索,所以我們先從生活中的數據說起。我們生活中的數據總體分爲兩種:

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

**非結構化數據:**又可稱爲全文數據,不定長或無固定格式,不適於由數據庫二維表來表現,包括所有格式的辦公文檔、XML、HTML、Word 文檔,郵件,各類報表、圖片和咅頻、視頻信息等。

**說明:**如果要更細緻的區分的話,XML、HTML 可劃分爲半結構化數據。因爲它們也具有自己特定的標籤格式,所以既可以根據需要按結構化數據來處理,也可抽取出純文本按非結構化數據來處理。

根據兩種數據分類,搜索也相應的分爲兩種:

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

對於非結構化數據,也即對全文數據的搜索主要有兩種方法:

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

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

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

**全文搜索:**對非結構化數據順序掃描很慢,我們是否可以進行優化?把我們的非結構化數據想辦法弄得有一定結構不就行了嗎?

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

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

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

先說說 Lucene

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

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

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

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

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

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

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

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

爲了創建倒排索引,我們通過分詞器將每個文檔的內容域拆分成單獨的詞(我們稱它爲詞條或 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
-------------------------------------

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

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

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

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

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

詞典和倒排表是 Lucene 中很重要的兩種數據結構,是實現快速檢索的重要基石。詞典和倒排文件是分兩部分存儲的,詞典在內存中而倒排文件存儲在磁盤上。

ES 核心概念

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

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

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

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

官網對 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 競爭。

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

爲了避免腦裂現象的發生,我們可以從原因着手通過以下幾個方面來做出優化措施:

分片(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 節點上的分片不再是主分片而是副本分片了,如果期間有更改的數據只需要從主分片上覆制修改的數據文件即可。

小結:

映射(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/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 的基本概念和基本操作介紹完了之後,我們可能還有很多疑惑:

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

寫索引原理

下圖描述了 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。

則具體流程如下:

存儲原理

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

具體的存儲路徑可在配置文件 ../config/elasticsearch.yml 中進行設置,默認存儲在安裝目錄的 Data 文件夾下。

建議不要使用默認值,因爲若 ES 進行了升級,則有可能導致數據全部丟失:

path.data: /path/to/data  //索引數據
path.logs: /path/to/logs  //日誌記錄

①分段存儲

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

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

段被寫入到磁盤後會生成一個提交點,提交點是一個用來記錄所有提交後段信息的文件。

一個段一旦擁有了提交點,就說明這個段只有讀的權限,失去了寫的權限。相反,當段在內存中時,就只有寫的權限,而不具備讀數據的權限,意味着不能被檢索。

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

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

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

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

段的不變性的缺點如下:

②延遲寫策略

介紹完了存儲的形式,那麼索引寫入到磁盤的過程是怎樣的?是否是直接調 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),事務日誌記錄了所有還沒有持久化到磁盤的數據。

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

通過這種方式當斷電或需要重啓時,ES 不僅要根據提交點去加載已經持久化過的段,還需要工具 Translog 裏的記錄,把未持久化的數據重新持久化到磁盤上,避免了數據丟失的可能。

③段合併

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

每一個段都會消耗文件句柄、內存和 CPU 運行週期。更重要的是,每個搜索請求都必須輪流檢查每個段然後合併查詢結果,所以段越多,搜索也就越慢。

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

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

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

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

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

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

性能優化

存儲設備

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

這裏有一些優化磁盤 I/O 的技巧:

內部索引優化

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 資源。

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

調整配置參數

調整配置參數建議如下:

JVM 調優

JVM 調優建議如下:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zPjubE95e_jy8YPYKZDyEQ