1. 程式人生 > >elasticsearch之shard內部

elasticsearch之shard內部

shard是什麼?它是如何工作的?這一章節我們將回答以下問題:

為什麼search是準實時的?

為什麼文件的CURD操作是實時的?

ES如何確保changes是持久話的的,即使斷電也不會丟失?

為什麼刪除文件並不立刻釋放儲存空間?

refresh,flush,optimize api是做什麼的?什麼時候應該使用?

1:making text searchable

傳統的database在一個field中儲存單獨的value(可能是多個word),這對全文索引顯然是不夠的。field中的每一個word都必須是searchable的,這意味著database需要在一個field中索引多個word。

滿足multiple-values-per-field這種需求的資料結構是inverted index。它儲存了一個在document中出現的term的有序列表,每一個term對應一個list,儲存了含有這個term的document。

Term  | Doc 1 | Doc 2 | Doc 3 | ...
------------------------------------
brown |   X   |       |  X    | ...
fox   |   X   |   X   |  X    | ...
quick |   X   |   X   |       | ...
the   |   X   |       |  X    | ...
inverted index也會包含另外一些有效的資訊。比如:term在多少個文件中出現過,term在某一個文件中出現的次數,每一個文件中term的順序,文件的長度,所有文件的平均長度等等。es用這些統計資訊來表徵term的重要性,這些將在what is relevance章節討論。

在全文索引的前期,整個文件集合用一個大的inverted index來構建並寫入磁碟。只要新的index準備就緒,就取代舊的index,最近的變化資料就變成searchable了。

寫入磁碟的inverted index是不可改變的(immutable)。這個特性有很多重要的優勢:

不再需要lock操作,如果我們從來都不更新索引,也就沒有必要擔心併發的訪問。

一旦index資料被載入到kernal的檔案系統cache中,資料將放在哪裡,因為從來不會被改變。只要檔案系統cache有足夠記憶體,大多數的讀操作將會從記憶體讀取,而非從磁碟,會大幅度提升效能。

在index的生命週期中,其他的cache(比如filter cache)保持有效。無需在資料改變的時候重建,因為資料不會改變。

寫一個大的inverted index允許資料被壓縮,減少昂貴的磁碟io和記憶體需要快取的索引量。

當然,這種特性也有其缺陷。因為資料不可改變,所以如果想要新的文件變成searchable的,不得不重建整個索引。這就在index可以包含的資料量或者索引更新的頻率方便有有了限制。

2:dynamically updateable indices

通過1中的論述,我們急需解決的問題是:在不改變immutable特性的前提下如何使得index變成updatable。答案是:use more than one index。

不用重建整個索引,增加一個補充索引來反映最近發生的改變。每一個inverted index可以輪流查詢--從最老的索引開始,然後將結果合併。

Lucene中介紹了一個概念per-segment search。一個segment就是一個inverted index,但是現在“index”這個概念逐漸演化成一個segment集合+一個commit point(包含了所有已知的segment)。

一個lucene index在es中我們稱為一個shard,而es中的index則是一系列shard。當es執行search操作,會將請求傳送到這個index包含的所有shard上去,然後將沒一個shard上的執行結果蒐集起來作為最終的結果。

文件會是首先新增到記憶體的buffer中,新的記錄先進入記憶體的buffer中,準備提交。

per-segment search按照以下方式工作:

新文件在記憶體的index buffer中聚集起來

時常,這個buffer提交:一個新的segment(作為補充性的inverted index),寫入磁碟;一個新的commit point寫入磁碟,包含了新產生的segment的名稱;執行fsync,所有在檔案系統cache中內容都寫入磁碟確保真正的寫入磁碟。

新產生的segment開啟,它所包含的文件searchable

記憶體中的buffer清空,準備接收新的文件。

完畢。

當一個query請求執行的時候,會在所有已知的segment中去執行查詢。Term statistics在所有segment中會聚合,以確保相關性的計算的準確性。按照以上的方式,新文件新增到索引中的代價比較低廉。

關於delete和update:

由於index是immutable的,所有文件不能從舊的segment中移除,同樣也不能更新。Instead,沒一個commit point都包括一個.del檔案,這個檔案包含了這個segment中所有被刪除的文件。當某一個document被deleted的時候,僅僅是在.del中記錄了這個文件。當然這個文件仍然可以被匹配到,只是在最終的返回結果中會被過濾掉。更新機制類似,一個比較老的version被記錄在.del中,新的version會記錄在新的segment中。當新老version都match的時候,老版本會從結果中排除掉。

在Segment merging這一章節中,會介紹是如何真正刪除文件的。

3:near real-time search

2中介紹的per-segment機制中,在index和search之間的時間間隔已經得到了很大的改善,新的文件可以在分鐘級別變為searchable,但是仍然不夠快速。

瓶頸在於磁碟。提交一個新的segment到磁碟需要執行fsync來確保真正的寫入磁碟,這樣即使斷電資料也不會丟失。但是fsync代價也是大的。需要一個更為輕量級的操作來是得新的文件可以searchable。

在es和磁碟之間是檔案系統的cache。之前介紹的記憶體中的index buffer將會寫入磁碟形成一個新的segment。但是新的segment會首先寫到檔案系統的cache中,這個過程是輕量級的,然後會寫入磁碟,這個過程是重量級的。但是當一個檔案已經存在與cache中,就可以被開啟並且searchable。

lucene允許新的segment被開啟並且searchable,而無需執行一個full commit操作。這個過程變的非常輕量級而且可以較為頻繁的執行。

在es中,這個輕量級的操作成為refresh,寫入並且開啟一個segment。預設情況下,每一個shard每隔1s會執行一次refresh操作。這也就是我們說es是near real-time search的原因:doc會在1s之內變成searchable的。

注意:refresh雖然比commit要輕,但仍然有一定的效能損耗,在test過程中可以手動執行refresh,但是在生產環境中,不能每索引一個doc就執行一次refresh,會損耗效能。

並不是所有的應用場景都需要每秒都執行refresh操作。也許你用es對大量的log file進行索引,這樣你會更傾向於提升索引的速度而不是變得near real-time search,所以,你可以增大refresh的間隔:

PUT /my_logs
{
  "settings": {
    "refresh_interval": "30s" 
  }
}
這個設定可以動態調整,甚至可以設定為-1,禁止掉,在完成索引之後重新設定為1s即可。

4:making changes persistent

如果沒有fsync操作把資料從檔案系統cacheflush到disk中,無法保證在斷電之後資料不丟失,哪怕是程序正常退出。es需要確保所有的更改都要持久化到磁碟中。

在3中介紹了full commit會把segment資料flush到磁碟並且生成一個commit point包含了所有可見的segment。es在啟動或者reopen index的過程中來確定哪些segment屬於這個shard。es會每秒執行一次refresh來確保near real-time search,同樣也需要定期執行full commit來確保可以災難恢復。但是在full commit間隔之間如果發生了資料的變化,而且好這個時間間隔又出現了意外,怎麼辦呢?我們不想丟失這部分的修改。

es用translog來解決這個問題,translog記錄了每一次操作。攜帶translog,執行流程是這樣的:

索引一個文件的時候,加入in-memory buffer,並且寫入translog;每一秒執行一次refresh操作,in-memory buffer中的doc寫成新的segment,並不執行fsync,segment open並且searchable,in-memory buffer清空,但是translog並不清空;持續的新資料新增到in-memory buffer中,並且寫入translog;當translog逐漸變大到一個閾值,執行flush操作,產生一個新的translog,並且執行了full commit(in-memory buffer中的docs寫入一個新的segment,buffer清空,commit point寫入disk,檔案系統cache執行fsync,translog刪除,生成新的translog)。

對於沒有flush到磁碟的所有操作,translog提供了一種持久化的儲存方式。當啟動的時候,系統會利用最後一個commit point來執行recovery,並且使用translog來回放在最後一個commit之後的操作。

translog同樣需要支援實時的real-time CURD操作。當通過id執行retrieve,update或者delete一個doc,首先會在translog中檢視最近的改變資料,然後才會在相關的segment中檢索。

es提供了flush api去執行flush操作,但是你幾乎不用手動執行flush操作,預設的設定足夠了。

在你關閉一個node時候執行一次flush操作會從中受益。當es執行recovery的時候,會從translog中回放操作,因此translog越小,recovery的時間就會越少。

下面問題來了:translog安全麼?

translog的目的就是確保操作不會丟失。但是translog也是檔案,也會有同樣的關於fsync的問題。預設情況下,translog每5s執行一次flush。因此,如果translog是唯一的機制的話,我們可能會丟失5s的資料。幸運的是,translog只是es這個龐大系統的一部分。請注意:一個index操作只有在primary shard和replica shard上都執行成功才算最終的成功。即使primary shard遇到災難性的損壞,不大可能影響到replica所在節點。雖然我們可以讓translog執行fsync更加頻繁(犧牲了效能),但是這樣做也不大可能提供更好的可靠性。
5:segment merging

自動的refresh程序每一秒鐘就會產生一個segment,因此過不了多長時間segment的數量就會膨脹。太多segment也是一個嚴重的問題。每一個segment都要消耗file handle,記憶體和cpu週期。更重要的是,每一個search請求都會去詢問每一個segment。因此segment越多,search速度越慢。

es通過在後臺merge的方式來解決這個問題,小的segment合併成大的segment,大的segment合併成更大的segment。

你並不需要啟用merge操作,這個過程在你index和search的時候是自動發生的。就像這樣工作:

當index過程中,refresh產生新的segment並open它們使得searchable;merge程序會在後臺選擇小的segments然後合併成較大的segment,這個過程並不會打斷index和search。當merge結束,老的segment就會刪除,就像這樣工作:

merge生成的新的segment寫入磁碟;生成新的commit point,包含新的segment同時排除舊的segment;新的segment open for search;老的segment被刪除。

對較大的segment的merge操作會佔用大量的cpu和io,會影響到search的效能。預設情況下,es對merge程序做了限制,以保證search程序會有足夠的資源來順利執行。

optimize api提供了強制執行merge的介面。它會讓一個shard合併到max_num_segments個segment。這樣做的原因就是要減少segment的數目(通常減少到1個)來提升search的效能。注意:optimize api不應該在動態索引(索引還在更新中)中使用,後臺的merge程序預設情況下工作狀況是非常良好的,而optimize api會妨礙這個程序的執行,所以不要干涉!

在一些特定的應用場景下,optimize api是益處良多的。一個典型的應用場景是就是logging,log資料按天,按周,按月建立索引。老的索引是read-only的,它們幾乎不會被改變.這種情況下,老索引可以呼叫optimize api來合併成一個segment,這樣它們會使用較少的資源,search效能也會提升。

POST /logstash-2014-10/_optimize?max_num_segments=1

注意:用optimize api引發的merge是沒有做任何限制的。會消耗掉所有的io資源,不會給search留下資源,因此叢集會變成unresponsive。如果機會對一個index進行optimize,應該使用shard allocation,首先將index移動到一個safe的node上執行。