Elasticsearch索引的精髓:一切設計都是為了提高搜索的性能。
二、設計原理
2.1 一個空的集群
Figure1 Empty Cluster
如果我們啟動了一個節點,沒有索引沒有數據,那么看起來就像上圖一樣。 一個節點Node運行著ES的實例,一個集群由一個或多個使用著同樣名字(cluster.name)的節點組成,分享數據和負載。 當Node從集群中添加或刪除時,集群會重組自己,使數據平攤的更均勻。
集群中需要有一臺master節點。master的作用是掌管集群的管理工作: 例如創建和刪除索引。 從集群中添加或刪除一臺節點。 master節點無需掌管文檔級的變更和索引。這也意味著在只有一臺master的情況下,隨著負載的增加master不會成為瓶頸。 所有的節點都可能變成master。
作為user,我們可以與任何一臺節點通信,包括master。每一臺節點都知道每一個文檔的位置并且可以將user的請求路由到文檔所在的節點,并且這臺節點負責接收它路由到的node or nodes的響應,并且將數據組織起來返回給用戶。這些對用戶都是透明的。
2.2 創建一個索引—index,shard,cluster
將數據添加到ES的前提是,我們需要一個索引(名詞):index——一個存儲與這個索引相對應數據的地方。實際上,index僅僅只是一個命名空間來指向一個或多個實際的物理分片(shard)。
一個分片(shard)是一個比較低層的工作單元來處理這個索引(index)的所有數據的一個切片(slice)。一個shard實際上是一個Lucene實例,在它的能力范圍內擁有完整的搜索功能(在處理它自己擁有的數據時有所有的功能)。我們所有文檔的索引indexed(動詞)和存儲工作都是在shard上,但這是透明的,我們不需要直接和shard通信,而是和我們創建的index(名詞)通信。
shards是ES將數據分布式在你的集群的關鍵。想象下shards是數據的容器,文檔存儲在shards里,而shards被分配在集群的每一個節點Node里。當你的集群規模增長和降低時,ES會自動的在Nodes間遷移shards以保持集群的負載均衡。
shard的分類與作用:
shard可分為primary shard和replica shard。 在一個index里的每一個文檔都屬于一個單獨的primary shard,所以primary shard的數量決定了你最大能存儲的數據量(對應于一個index)。
注意:shard是歸屬與index的,而不是cluster的。
replica shard是primary shard的拷貝。replica有兩個作用: 1.冗余容災 2.提供讀請求服務,例如搜索或讀取文檔
primary shard的數量在索引創建時確定后不能修改,replica可以在任何時候修改。 例: 見Figure2,在2.1的集群上創建一個index,擁有3個primary shards以及1個replica shards。
由于只有一臺Node,而Primary shard的Replicas與其在同一臺節點上毫無意義,所以集群沒有初始化replicas,這時添加另外一臺Node。見Figure3,每一個primary shard初始化了一個replica。
2.3 水平擴容
當我們繼續添加一臺節點時,Node1和Node2中的各取一個shard移動到了Node3.見Figure4
這樣,我們每一臺Node上只有兩個shard。這就意味著每一臺Node的硬件資源(CPU,RAM,I/O)將會被更少的shards共享,提高了每一個shard的性能。在這個案例中,6個shards最多可使用6臺Node,這樣每個shard就可以使用100%的node硬件資源。
現在我們修改replica的數量到2,如Figure5
這樣我們就有了一個3primary shards,6replica shards的Cluster。我們可將Node提高到9臺。水平擴容了集群性能。
2.4 容災
ES可以容下當節點宕機情況下的異常。例如現在我們殺掉Node1節點。見Figure6
我們殺掉的是master節點。一個Cluster必須要有master以保證集群的功能正常。所以集群要做的第一件事是選擇一個新的master:Node2. 當我們殺掉1節點時,Primary shards 1和2丟失了。如果丟失了primary shard,index(名詞)將不能正常的工作。此時P1和P2的拷貝存在Node2和Node3上。所以此時新升級的master(Node2)將做的第一件事就是將NODE2和NODE3上的replica shard1和replica shard2升級為primary shard。此時如果我們殺掉NODE2,整個集群的容災過程同理,還是可以正常運行。
這時,如果我們重啟了NODE1,cluster將會重新分配缺少的兩個replica shards(現在每個primary shard只有2個replicas,配置是3個,缺少2個)。如果NODE1的數據是舊的,那么它將會繼續利用它們,NODE1只會從現在的Primary Shards拷貝這期間更改的數據。
2.5 分布式文檔存儲
2.5.1 Shards文檔路由
當你對一個文檔建立索引時,它僅存儲在一個primary shard上。ES是怎么知道一個文檔應該屬于哪個shard?當你創建一個新的文檔時,ES是怎么知道應該把它存儲至shard1還是shard2? 這個過程不能隨機無規律的,因為以后我們還要將它取出來。它的路由算法是:
shard = hash(routing) % numberofprimary_shards
routing的值可以是文檔的id,也可以是用戶自己設置的一個值。hash將會根據routing算出一個數值然后%primaryshards的數量。這也是為什么primary_shards在index創建時就不能修改的原因。
問題:當看到這里時,產生了一個問題:ES為什么要這樣設計路由算法,這樣就強制使primaryshards不可變,不利于以后index的擴容,除非事前就要對數據規模有所評估來設計可擴展的index。為什么不能使用一致性hash解決primaryshards改變時的情況呢?
2.5.2 Primary/Replica Shards的交互
假如我們有Figure8的集群。我們可以向這個集群的任何一臺NODE發送請求,每一個NODE都有能力處理請求。每一個NODE都知道每一個文檔所在的位置所以可以直接將請求路由過去。下面的例子,我們將所有的請求都發送到NODE1。
注:最好的實踐方式是輪詢所有的NODE來發送請求,以達到請求負載均衡。
寫操作
創建、索引、刪除文檔都是寫操作,這些操作必須在primary shard完全成功后才能拷貝至其對應的replicas上。見Figure9。
下面是Figure9的步驟:
1.客戶端向Node1發送寫操作的請求。
2.Node1使用文檔的_id來決定這個文檔屬于shard0,然后將請求路由至NODE3,P0所在的位置。
3.Node3在P0上執行了請求。如果請求成功,則將請求并行的路由至NODE1 NODE2的R0上。當所有的replicas報告成功后,NODE3向請求的node(NODE1)發送成功報告,NODE1再報告至Client。
當客戶端收到執行成功后,操作已經在Primary shard和所有的replica shards上執行成功了。
當然,有一些請求參數可以修改這個邏輯。見原文。
讀操作
一個文檔可以在primary shard和所有的replica shard上讀取。見Figure10
讀操作步驟:
1.客戶端發送Get請求到NODE1。
2.NODE1使用文檔的_id決定文檔屬于shard 0.shard 0的所有拷貝存在于所有3個節點上。這次,它將請求路由至NODE2。
3.NODE2將文檔返回給NODE1,NODE1將文檔返回給客戶端。 對于讀請求,請求節點(NODE1)將在每次請求到來時都選擇一個不同的replica。
shard來達到負載均衡。使用輪詢策略輪詢所有的replica shards。
更新操作
更新操作,結合了以上的兩個操作:讀、寫。見Figure11
步驟:
1.客戶端發送更新操作請求至NODE1
2.NODE1將請求路由至NODE3,Primary shard所在的位置
3.NODE3從P0讀取文檔,改變source字段的JSON內容,然后試圖重新對修改后的數據在P0做索引。如果此時這個文檔已經被其他的進程修改了,那么它將重新執行3步驟,這個過程如果超過了retryon_conflict設置的次數,就放棄。
4.如果NODE3成功更新了文檔,它將并行的將新版本的文檔同步到NODE1和NODE2的replica shards重新建立索引。一旦所有的replica
shards報告成功,NODE3向被請求的節點(NODE1)返回成功,然后NODE1向客戶端返回成功。
2.6 Shard
本節將解決以下問題:
為什么搜索是實時的
為什么文檔的CRUD操作是實時的
ES怎么保障你的更新在宕機的時候不會丟失
為什么刪除文檔不會立即釋放空間
2.6.1不變性
寫到磁盤的倒序索引是不變的:自從寫到磁盤就再也不變。 這會有很多好處:
不需要添加鎖。如果你從來不用更新索引,那么你就不用擔心多個進程在同一時間改變索引。
一旦索引被內核的文件系統做了Cache,它就會待在那因為它不會改變。只要內核有足夠的緩沖空間,絕大多數的讀操作會直接從內存而不需要經過磁盤。這大大提升了性能。
其他的緩存(例如fiter cache)在索引的生命周期內保持有效,它們不需要每次數據修改時重構,因為數據不變。
寫一個單一的大的倒序索引可以讓數據壓縮,減少了磁盤I/O的消耗以及緩存索引所需的RAM。
當然,索引的不變性也有缺點。如果你想讓新修改過的文檔可以被搜索到,你必須重新構建整個索引。這在一個index可以容納的數據量和一個索引可以更新的頻率上都是一個限制。
2.6.2動態更新索引
如何在不丟失不變形的好處下讓倒序索引可以更改?答案是:使用不只一個的索引。 新添額外的索引來反映新的更改來替代重寫所有倒序索引的方案。 Lucene引進了per-segment搜索的概念。一個segment是一個完整的倒序索引的子集,所以現在index在Lucene中的含義就是一個segments的集合,每個segment都包含一些提交點(commit point)。見Figure16。新的文檔建立時首先在內存建立索引buffer,見Figure17。然后再被寫入到磁盤的segment,見Figure18。
一個per-segment的工作流程如下:
1.新的文檔在內存中組織,見Figure17。
2.每隔一段時間,buffer將會被提交: 一個新的segment(一個額外的新的倒序索引)將被寫到磁盤 一個新的提交點(commit point)被寫入磁盤,將包含新的segment的名稱。 磁盤fsync,所有在內核文件系統中的數據等待被寫入到磁盤,來保障它們被物理寫入。
3.新的segment被打開,使它包含的文檔可以被索引。
4.內存中的buffer將被清理,準備接收新的文檔。
當一個新的請求來時,會遍歷所有的segments。詞條分析程序會聚合所有的segments來保障每個文檔和詞條相關性的準確。通過這種方式,新的文檔輕量的可以被添加到對應的索引中。
刪除和更新
segments是不變的,所以文檔不能從舊的segments中刪除,也不能在舊的segments中更新來映射一個新的文檔版本。取之的是,每一個提交點都會包含一個.del文件,列舉了哪一個segmen的哪一個文檔已經被刪除了。 當一個文檔被”刪除”了,它僅僅是在.del文件里被標記了一下。被”刪除”的文檔依舊可以被索引到,但是它將會在最終結果返回時被移除掉。
文檔的更新同理:當文檔更新時,舊版本的文檔將會被標記為刪除,新版本的文檔在新的segment中建立索引。也許新舊版本的文檔都會本檢索到,但是舊版本的文檔會在最終結果返回時被移除。
2.6.3實時索引
在上述的per-segment搜索的機制下,新的文檔會在分鐘級內被索引,但是還不夠快。 瓶頸在磁盤。將新的segment提交到磁盤需要fsync來保障物理寫入。但是fsync是很耗時的。它不能在每次文檔更新時就被調用,否則性能會很低。 現在需要一種輕便的方式能使新的文檔可以被索引,這就意味著不能使用fsync來保障。 在ES和物理磁盤之間是內核的文件系統緩存。之前的描述中,Figure19,Figure20,在內存中索引的文檔會被寫入到一個新的segment。但是現在我們將segment首先寫入到內核的文件系統緩存,這個過程很輕量,然后再flush到磁盤,這個過程很耗時。但是一旦一個segment文件在內核的緩存中,它可以被打開被讀取。
2.6.4更新持久化
不使用fsync將數據flush到磁盤,我們不能保障在斷電后或者進程死掉后數據不丟失。ES是可靠的,它可以保障數據被持久化到磁盤。 在2.6.2中,一個完全的提交會將segments寫入到磁盤,并且寫一個提交點,列出所有已知的segments。當ES啟動或者重新打開一個index時,它會利用這個提交點來決定哪些segments屬于當前的shard。 如果在提交點時,文檔被修改會怎么樣?不希望丟失這些修改:
1.當一個文檔被索引時,它會被添加到in-memory buffer,并且添加到Translog日志中,見Figure21.
2.refresh操作會讓shard處于Figure22的狀態:每秒中,shard都會被refreshed:
在in-memory buffer中的文檔會被寫入到一個新的segment,但沒有fsync。
in-memory buffer被清空
3.這個過程將會持續進行:新的文檔將被添加到in-memory buffer和translog日志中,見Figure23
4.一段時間后,當translog變得非常大時,索引將會被flush,新的translog將會建立,一個完全的提交進行完畢。見Figure24
在in-memory中的所有文檔將被寫入到新的segment
內核文件系統會被fsync到磁盤。
舊的translog日志被刪除
translog日志提供了一個所有還未被flush到磁盤的操作的持久化記錄。當ES啟動的時候,它會使用最新的commit point從磁盤恢復所有已有的segments,然后將重現所有在translog里面的操作來添加更新,這些更新發生在最新的一次commit的記錄之后還未被fsync。
translog日志也可以用來提供實時的CRUD。當你試圖通過文檔ID來讀取、更新、刪除一個文檔時,它會首先檢查translog日志看看有沒有最新的更新,然后再從響應的segment中獲得文檔。這意味著它每次都會對最新版本的文檔做操作,并且是實時的。
2.6.5 Segment合并
通過每隔一秒的自動刷新機制會創建一個新的segment,用不了多久就會有很多的segment。segment會消耗系統的文件句柄,內存,CPU時鐘。最重要的是,每一次請求都會依次檢查所有的segment。segment越多,檢索就會越慢。
ES通過在后臺merge這些segment的方式解決這個問題。小的segment merge到大的,大的merge到更大的。。。
這個過程也是那些被”刪除”的文檔真正被清除出文件系統的過程,因為被標記為刪除的文檔不會被拷貝到大的segment中。
合并過程如Figure25:
1.當在建立索引過程中,refresh進程會創建新的segments然后打開他們以供索引。
2.merge進程會選擇一些小的segments然后merge到一個大的segment中。這個過程不會打斷檢索和創建索引。
3.Figure26,一旦merge完成,舊的segments將被刪除
新的segment被flush到磁盤
一個新的提交點被寫入,包括新的segment,排除舊的小的segments
新的segment打開以供索引
舊的segments被刪除
merge大的segments會消耗大量的I/O和CPU,嚴重影響索引性能。默認,ES會節制merge過程來給留下足夠多的系統資源。
核心概念
集群(Cluster):?ES集群是一個或多個節點的集合,它們共同存儲了整個數據集,并提供了聯合索引以及可跨所有節點的搜索能力。多節點組成的集群擁有冗余能力,它可以在一個或幾個節點出現故障時保證服務的整體可用性。
集群靠其獨有的名稱進行標識,默認名稱為“elasticsearch”。節點靠其集群名稱來決定加入哪個ES集群,一個節點只能屬一個集群。
節點(node):?一個節點是一個邏輯上獨立的服務,可以存儲數據,并參與集群的索引和搜索功能, 一個節點也有唯一的名字,群集通過節點名稱進行管理和通信.
主節點:主節點的主要職責是和集群操作相關的內容,如創建或刪除索引,跟蹤哪些節點是群集的一部分,并決定哪些分片分配給相關的節點。穩定的主節點對集群的健康是非常重要的。雖然主節點也可以協調節點,路由搜索和從客戶端新增數據到數據節點,但最好不要使用這些專用的主節點。一個重要的原則是,盡可能做盡量少的工作。
對于大型的生產集群來說,推薦使用一個專門的主節點來控制集群,該節點將不處理任何用戶請求。
數據節點:持有數據和倒排索引。
客戶端節點:它既不能保持數據也不能成為主節點,該節點可以響應用戶的情況,把相關操作發送到其他節點;客戶端節點會將客戶端請求路由到集群中合適的分片上。對于讀請求來說,協調節點每次會選擇不同的分片處理請求,以實現負載均衡。
部落節點:部落節點可以跨越多個集群,它可以接收每個集群的狀態,然后合并成一個全局集群的狀態,它可以讀寫所有節點上的數據。
索引(Index):?ES將數據存儲于一個或多個索引中,索引是具有類似特性的文檔的集合。類比傳統的關系型數據庫領域來說,索引相當于SQL中的一個數據庫,或者一個數據存儲方案(schema)。索引由其名稱(必須為全小寫字符)進行標識,并通過引用此名稱完成文檔的創建、搜索、更新及刪除操作。一個ES集群中可以按需創建任意數目的索引。
文檔類型(Type):類型是索引內部的邏輯分區(category/partition),然而其意義完全取決于用戶需求。因此,一個索引內部可定義一個或多個類型(type)。一般來說,類型就是為那些擁有相同的域的文檔做的預定義。例如,在索引中,可以定義一個用于存儲用戶數據的類型,一個存儲日志數據的類型,以及一個存儲評論數據的類型。類比傳統的關系型數據庫領域來說,類型相當于“表”。
文檔(Document):文檔是Lucene索引和搜索的原子單位,它是包含了一個或多個域的容器,基于JSON格式進行表示。文檔由一個或多個域組成,每個域擁有一個名字及一個或多個值,有多個值的域通常稱為“多值域”。每個文檔可以存儲不同的域集,但同一類型下的文檔至應該有某種程度上的相似之處。相當于數據庫的“記錄”
Mapping:?相當于數據庫中的schema,用來約束字段的類型,不過 Elasticsearch 的 mapping 可以自動根據數據創建
ES中,所有的文檔在存儲之前都要首先進行分析。用戶可根據需要定義如何將文本分割成token、哪些token應該被過濾掉,以及哪些文本需要進行額外處理等等。
分片(shard) :ES的“分片(shard)”機制可將一個索引內部的數據分布地存儲于多個節點,它通過將一個索引切分為多個底層物理的Lucene索引完成索引數據的分割存儲功能,這每一個物理的Lucene索引稱為一個分片(shard)。
每個分片其內部都是一個全功能且獨立的索引,因此可由集群中的任何主機存儲。創建索引時,用戶可指定其分片的數量,默認數量為5個。
Shard有兩種類型:primary和replica,即主shard及副本shard。
Primary shard用于文檔存儲,每個新的索引會自動創建5個Primary shard,當然此數量可在索引創建之前通過配置自行定義,不過,一旦創建完成,其Primary shard的數量將不可更改。
Replica shard是Primary Shard的副本,用于冗余數據及提高搜索性能。
每個Primary shard默認配置了一個Replica shard,但也可以配置多個,且其數量可動態更改。ES會根據需要自動增加或減少這些Replica shard的數量。
ES集群可由多個節點組成,各Shard分布式地存儲于這些節點上。