天天看點

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上執行。

繼續閱讀