Lucene是一種高性能、可伸縮的信息搜索(IR)庫,在2000年開源,最初由鼎鼎大名的Doug Cutting開發,是基於Java實現的高性能的開源項目。Lucene採用了基於倒排表的設計原理,能夠很是高效地實現文本查找,在底層採用了分段的存儲模式,使它在讀寫時幾乎徹底避免了鎖的出現,大大提高了讀寫性能。css
Elasticsearch基於lucene,隱藏其複雜性,並提供簡單易用的restful API接口、java API接口。因此理解ES的關鍵在於理解lucene的基本原理。java
Lucene的寫流程和讀流程如圖5-1所示。node
其中,虛線箭頭(a、b、c、d)表示寫索引的主要過程,實線箭頭(1-9)表示查詢的主要過程。算法
Lucene中的主要模塊(見圖5-1)及模塊說明以下:數據庫
下面介紹Lucene中的核心術語。數組
▪️Term:是索引裏最小的存儲和查詢單元,對於英文來講通常是指一個單詞,對於中文來講通常是指一個分詞後的詞。緩存
▪️詞典(Term Dictionary,也叫做字典):是Term的集合。詞典的數據結構能夠有不少種,每種都有本身的優缺點,好比:排序數組經過二分查找來檢索數據:HashMap(哈希表)比排序數組的檢索速度更快,可是會浪費存儲空間;fst(finite-state transducer)有更高的數據壓縮率和查詢效率,由於詞典是常駐內存的,而fst有很好的壓縮率,因此fst在Lucene的最新版本中有很是多的使用場景,也是默認的詞典數據結構。安全
▪️倒排序(Posting List):一篇文章一般由多個詞組成,倒排表記錄的是某個詞在哪些文章中出現過。性能優化
▪️正向信息:原始的文檔信息,能夠用來作排序、聚合、展現等。bash
▪️段(segment):索引中最小的獨立存儲單元。一個索引文件由一個或者多個段組成。在Luence中的段有不變性,也就是說段一旦生成,在其上只能有讀操做,不能有寫操做。
Lucene的底層存儲格式如圖5-2所示。圖5-2由詞典和倒排序兩部分組成,其中的詞典就是Term的集合。詞典中的Term指向的文檔鏈表的集合,叫作倒排表。詞典和倒排表是Lucene中很重要的兩種數據結構,是實現快速檢索的重要基石。詞典和倒排表是分兩部分存儲的,在倒排序中不但存儲了文檔編號,還存儲了詞頻等信息。
在圖5-2所示的詞典部分包含三個詞條(Term):elasticsearch、lucene和solr。詞典數據是查詢的入口,因此這部分數據是以fst的形式存儲在內存中的。
在倒排表中,「lucene」指向有序鏈表3,7,15,30,35,67,表示字符串「lucene」在文檔編號爲三、七、1五、30、3五、67的文章中出現過,elasticsearch和solr同理。
在Lucene的查詢過程當中的主要檢索方式有如下四種。
指對一個Term進行查詢。好比,若要查找包含字符串「lucene」的文檔,則只需在詞典中找到Term「lucene」,再得到在倒排表中對應的文檔鏈表便可。
指對多個集合求交集。好比,若要查找既包含字符串「lucene」又包含字符串「solr」的文檔,則查找步驟以下。
(1)在詞典中找到Term 「lucene」,獲得「lucene」對應的文檔鏈表。
(2)在詞典中找到Term 「solr」,獲得「solr」對應的文檔鏈表。
(3)合併鏈表,對兩個文檔鏈表作交集運算,合併後的結果既包含「lucene」也包含「solr」。
指多個集合求並集。好比,若要查找包含字符串「luence」或者包含字符串「solr」的文檔,則查找步驟以下。
(1)在詞典中找到Term 「lucene」,獲得「lucene」對應的文檔鏈表。
(2)在詞典中找到Term 「solr」,獲得「solr」對應的文檔鏈表。
(3)合併鏈表,對兩個文檔鏈表作並集運算,合併後的結果包含「lucene」或者包含「solr」。
指對多個集合求差集。好比,若要查找包含字符串「solr」但不包含字符串「lucene」的文檔,則查找步驟以下。
(1)在詞典中找到Term 「lucene」,獲得「lucene」對應的文檔鏈表。
(2)在詞典中找到Term 「solr」,獲得「solr」對應的文檔鏈表。
(3)合併鏈表,對兩個文檔鏈表作差集運算,用包含「solr」的文檔集減去包含「lucene」的文檔集,運算後的結果就是包含「solr」但不包含「lucene」。
經過上述四種查詢方式,咱們不難發現,因爲Lucene是以倒排表的形式存儲的,因此在Lucene的查找過程當中只需在詞典中找到這些Term,根據Term得到文檔鏈表,而後根據具體的查詢條件對鏈表進行交、並、差等操做,就能夠準確地查到咱們想要的結果,相對於在關係型數據庫中的「like」查找要作全表掃描來講,這種思路是很是高效的。雖然在索引建立時要作不少工做,但這種一次生成、屢次使用的思路也是很是高明的。
在早期的全文檢索中爲整個文檔集合創建了一個很大的倒排索引,並將其寫入磁盤中,若是索引有更新,就須要從新全量建立一個索引來替換原來的索引。這種方式在數據量很大時效率很低,而且因爲建立一次索引的成本很高,因此對數據的更新不能過於頻繁,也就不能保證明效性。
如今,在搜索中引入了段的概念(將一個索引文件拆分爲多個子文件,則每一個子文件叫作段),每一個段都是一個獨立的可被搜索的數據集,而且段具備不變性,一旦索引的數據被寫入硬盤,就不可修改。
在分段的思想下,對數據寫操做的過程以下。
段不可變性的優勢以下:
段不可變性的缺點以下:
爲了提高寫的性能,Lucene並無每新增一條數據就增長一個段,而是採用延遲寫的策略,每當有新增的數據時,就將其先寫入內存中,而後批量寫入磁盤中。如有一個段被寫到硬盤,就會生成一個提交點,提交點就是一個用來記錄全部提交後的段信息的文件。一個段一旦擁有了提交點,就說明這個段只有讀到權限,失去了寫的權限;相反,當段在內存中時,就只有寫數據的權限,而不具有讀數據的權限,因此也就不能被檢索了。從嚴格意義上來講,Lucene或者Elasticsearch並不能被稱爲實時的搜索引擎,只能被稱爲準實時的搜索引擎。
寫索引的流程以下:
(1)新數據被寫入時,並無被直接寫到硬盤中,而是被暫時寫到內存中。Lucene默認是一秒鐘,或者當內存中數據量達到必定階段時,再批量提交到磁盤中,固然,默認的時間和數據量的大小是能夠經過參數控制的。經過延時寫的策略,能夠減小數據往磁盤上寫的次數,從而提高總體的寫入性能。如圖5-7所示。
(2)在達到出觸發條件之後,會將內存中緩存的數據一次性寫入磁盤中,並生成提交點。
(3)狀況內存,等待新的數據寫入。如圖5-8所示。
從上述流程能夠看出,數據先被暫時緩存在內存中,在達到必定的條件再被一次性寫入硬盤中,這種作法能夠大大提高數據寫入的書單。可是數據先被暫時存放在內存中,並無真正持久化到磁盤中,因此若是這時出現斷電等不可控的狀況,就會丟失數據,爲此,Elasticsearch添加了事務日誌,來保證數據的安全,參見5.2.3節。
雖然分段比每次都全量建立索引有更高的效率,可是因爲在每次新增數據時都會新增一個段,因此通過長時間的的積累,會致使在索引中存在大量的段,當索引中段的數量太多時,不只會嚴重消耗服務器的資源,還會影響檢索的性能。
由於索引檢索的過程是:查詢全部段中知足查詢條件的數據,而後對每一個段裏查詢的結果集進行合併,因此爲了控制索引裏段的數量,咱們必須按期進行段合併操做。可是若是每次合併所有的段,則會形成很大的資源浪費,特別是「大段」的合併。因此Lucene如今的段合併思路是:根據段的大小將段進行分組,再將屬於同一組的段進行合併。可是因爲對於超級大的段的合併須要消耗更多的資源,因此Lucene會在段的大小達到必定規模,或者段裏面的數據量達到必定條數時,不會再進行合併。因此Lucene的段合併主要集中在對中小段的合併上,這樣既能夠避免對大段進行合併時消耗過多的服務器資源,也能夠很好地控制索引中段的數量。
段合併的主要參數以下:
段合併相關的動做主要有如下兩個:
在段合併前對段的大小進行了標準化處理,經過
logMergeFactorSegmentSize
計算得出,其中MergeFactor表示一次合併的段的數量,Lucene默認該數量爲10;SegmentSize表示段的實際大小。經過上面的公式計算後,段的大小更加緊湊,對後續的分組更加友好。
段分組的步驟以下:
(1)根據段生成的時間對段進行排序,而後根據上述標準化公式計算每一個段的大小而且存放到段信息中,後面用到的描述段大小的值都是標準化後的值。如圖5-9所示。
(2)在數組中找到最大的段,而後生成一個由最大段的標準化值做爲上線,減去LEVEL_LOG_SPAN(默認值爲0.75)後的值做爲下限的區間,小於等於上限而且大於下限的段,都被認爲是屬於同一組的段,能夠合併。
(3)在肯定一個分組的上下限值後,就須要查找屬於這個分組的段了,具體過程是:建立兩個指針(在這裏使用指針的概念是爲了更好地理解)start和end,start指向數組的第1個段,end指向第start+MergeFactor個段,而後從end逐個向前查找落在區間的段,當找到第1個知足條件的段時,則中止,並把當前段到start之間的段統一分到一個組,不管段的大小是否知足當前分組的條件。如圖5-10所示,第2個段明顯小於該分組的下限,但仍是被分到了這一組。
這樣作的好處以下:
(4)在分組找到後,須要排除不參加合併的「超大」段,而後判斷剩餘的段是否知足合併的條件,如圖5-10所示,mergeFactor=5,而找到的知足合併條件的段的個數爲4,因此不知足合併的條件,暫時不進行合併,繼續找尋下一個組的上下限。
(5)因爲在第4步並無找到知足段合併的段的數量,因此這一分組的段不知足合併的條件,繼續進行下一分組段的查找。具體過程是:將start指向end,在剩下的段(從end指向的元素開始到數組的最後一個元素)中中尋找最大的段,在找到最大的值後再減去LEVEL_LOG_SPAN的值,再生成一下分組的區間值;而後把end指向數組的第start+MergeFactor個段,逐個向前查找第1個知足條件的段:重複第3步和第4步。
(6)若是一直沒有找到知足合併條件的段,則一直重複第5步,直到遍歷完整個數組。若是如圖5-11所示。
(7)在找到知足條件的mergeFactor個段時,就須要開始合併了。可是在知足合併條件的段大於mergeFactor時,就須要進行屢次合併,也就是說每次依然選擇mergeFactor個段進行合併,直到該分組的全部段合併完成,再進行下一分組的查找合併操做。
(8)經過上述幾步,若是找到了知足合併要求的段,則將會進行段的合併操做。由於索引裏面包含了正向信息和反向信息,因此段合併的操做分爲兩部分:一個是正向信息合併,例如存儲域、詞向量、標準化因子等;一個是反向信息的合併,例如詞典、倒排表等。在段合併時,除了須要對索引數據進行合併,還須要移除段中已經刪除的數據。
咱們在前面瞭解到,Lucene的查詢過程是:首先在詞典中查找每一個Term,根據Term得到每一個Term所在的文檔鏈表;而後根據查詢條件對鏈表作交、並、差等操做,鏈表合併後的結果集就是咱們要查找的數據。這樣作能夠徹底避免對關係型數據庫進行全表掃描,能夠大大提高查詢效率。可是,當咱們一次查詢出不少數據時,這些數據和咱們的查詢條件又有多大關係呢?其文本類似度是多少?本節會回答這個問題,並介紹Lucene最經典的兩個文本類似度算法:基於向量空間模型的算法和基於機率的算法(BM25)。
若是對此算法不太感興趣,那麼只需瞭解對文本類似度有影響的因子有哪些,哪些是正向的,哪些是逆向的便可,不須要理解每一個算法的推理過程。可是這兩個文本類似度算法有很好的借鑑意義。
Elasticsearch是使用Java編寫的一種開源搜索引擎,它在內部使用Luence作索引與搜索,經過對Lucene的封裝,提供了一套簡單一致的RESTful API。Elasticsearch也是一種分佈式的搜索引擎架構,能夠很簡單地擴展到上百個服務節點,並支持PB級別的數據查詢,使系統具有高可用和高併發性。
Elasticsearch的核心概念以下:
node.master=false node.data=false
tribe: one: cluster.name: cluster_one two: cluster.name: cluster_two
由於Tribe Node要在Elasticsearch 7.0之後移除,因此不建議使用。
共識性是分佈式系統中最基礎也最主要的一個組件,在分佈式系統中的全部節點必須對給定的數據或者節點的狀態達成共識。雖然如今有很成熟的共識算法如Raft、Paxos等,也有比較成熟的開源軟件如Zookeeper。可是Elasticsearch並無使用它們,而是本身實現共識系統zen discovery。Elasticsearch之父Shay Banon解釋了其中主要的緣由:「zen discovery是Elasticsearch的一個核心的基礎組件,zen discovery不只可以實現共識系統的選擇工做,還可以很方便地監控集羣的讀寫狀態是否健康。固然,咱們也不保證其後期會使用Zookeeper代替如今的zen discovery」。zen discovery模塊以「八卦傳播」(Gossip)的形式實現了單播(Unicat):單播不一樣於多播(Multicast)和廣播(Broadcast)。節點間的通訊方式是一對一的。
Elasticsearch是一個分佈式系統。寫請求在發送到主分片時,同時會以並行的形式發送到備份分片,可是這些請求的送達時間多是無序的。在這種狀況下,Elasticsearch用樂觀併發控制(Optimistic Concurrency Control)來保證新版本的數據不會被舊版本的數據覆蓋。
樂觀併發控制是一種樂觀鎖,另外一種經常使用的樂觀鎖即多版本併發控制(Multi-Version Concurrency Control),它們的主要區別以下:
Elasticsearch集羣保證寫一致性的方式是在寫入前先檢查有多少個分片可供寫入,若是達到寫入條件,則進行寫操做,不然,Elasticsearch會等待更多的分片出現,默認爲一分鐘。
有以下三種設置來判斷是否容許寫操做:
在Elasticsearch集羣中主節點經過ping命令來檢查集羣中的其餘節點是否處於可用狀態,同時非主節點也會經過ping來檢查主節點是否處於可用狀態。當集羣網絡不穩定時,有可能會發生一個節點ping不通Master節點,則會認爲Master節點發生了故障,而後從新選出一個Master節點,這就會致使在一個集羣內出現多個Master節點。當在一個集羣中有多個Master節點時,就有可能會致使數據丟失。咱們稱這種現象爲腦裂。在5.4.7節會介紹如何避免腦裂的發生。
咱們在5.1節瞭解到,Lucene爲了加快寫索引的速度,採用了延遲寫入的策略。雖然這種策略提升了寫入的效率,但其最大的弊端是,若是數據在內存中尚未持久化到磁盤上時發生了相似斷電等不可控狀況,就可能丟失數據。爲了不丟失數據,Elasticsearch添加了事務日誌(Translog),事務日誌記錄了全部尚未被持久化磁盤的數據。
Elasticsearch寫索引的具體過程以下。
首先,當有數據寫入時,爲了提高寫入的速度,並無數據直接寫在磁盤上,而是先寫入到內存中,可是爲了防止數據的丟失,會追加一份數據到事務日誌裏。由於內存中的數據還會繼續寫入,因此內存中的數據並非以段的形式存儲的,是檢索不到的。總之,Elasticsearch是一個準實時的搜索引擎,而不是一個實時的搜索引擎。此時的狀態如圖5-14所示。
而後,當達到默認的時間(1秒鐘)或者內存的數據達到必定量時,會觸發一次刷新(Refresh)。刷新的主要步驟以下。
(1)將內存中的數據刷新到一個新的段中,可是該段並無持久化到硬盤中,而是緩存在操做系統的文件緩存系統中。雖然數據還在內存中,可是內存裏的數據和文件緩存系統裏的數據有如下區別。
(2)打開保存在文件緩存系統中的段,使其可被搜索。
(3)清空內存,準備接收新的數據。日誌不作清空處理。
此時的狀態如圖5-15所示。
最後,刷新(Flush)。當日志數據的大小超過512MB或者時間超過30分鐘時,須要觸發一次刷新。刷新的主要步驟以下。
刪除舊的日誌,建立一個空的日誌。
此時的狀態如圖5-17所示。
由上面索引建立的過程可知,內存裏面的數據並無直接被刷新(Flush)到硬盤中,而是被刷新(Refresh)到了文件緩存系統中,這主要是由於持久化數據十分耗費資源,頻繁地調用會使寫入的性能急劇降低,因此Elasticsearch,爲了提升寫入的效率,利用了文件緩存系統和內存來加速寫入時的性能,並使用日誌來防止數據的丟失。
在須要重啓時,Elasticsearch不只要根據提交點去加載已經持久化過的段,還須要根據Translog裏的記錄,把未持久化的數據從新持久化到磁盤上。
根據上面對Elasticsearch,寫操做流程的介紹,咱們能夠整理出一個索引數據所要經歷的幾個階段,以及每一個階段的數據的存儲方式和做用。如圖5-18所示。
假設咱們有如圖5-19所示(圖片來自官網)的一個集羣,該集羣由三個節點組成(Node 一、Node 2和Node 3),包含一個由兩個主分片和每一個主分片由兩個副本分片組成的索引。其中,標星號的Node 1是Master節點,負責管理整個集羣的狀態;p1和p2是主分片;r0和r1是副本分片。爲了達到高可用,Master節點避免將主分片和副本放在同一個節點。
將數據分片是爲了提升可處理數據的容量和易於進行水平擴展,爲分片作副本是爲了提升集羣的穩定性和提升併發量。在主分片掛掉後,會從副本分片中選舉出一個升級爲主分片,當副本升級爲主分片後,因爲少了一個副本分片,因此集羣狀態會從green改變爲yellow,可是此時集羣仍然可用。在一個集羣中有一個分片的主分片和副本分片都掛掉後,集羣狀態會由yellow改變爲red,集羣狀態爲red時集羣不可正常使用。
由上面的步驟可知,副本分片越多,集羣的可用性就越高,可是因爲每一個分片都至關於一個Lucene的索引文件,會佔用必定的文件句柄、內存及CPU,而且分片間的數據同步也會佔用必定的網絡帶寬,因此,索引的分片數和副本數並非越多越好。
寫索引時只能寫在主分片上,而後同步到副本上,那麼,一個數據應該被寫在哪一個分片上呢?如圖5-19所示,如何知道一個數據應該被寫在p0仍是p1上呢答案就是路由(routing),路由公式以下:
shard = hash(routing)%number_of_primary_shards
其中,routing是一個可選擇的值,默認是文檔的_id(文檔的惟一主鍵,文檔在建立時,若是文檔的_id已經存在,則進行更新,若是不存在則建立)。後面會介紹如何經過自定義routing參數使查詢落在一個分片中,而不用查詢全部的分片,從而提高查詢的性能。routing經過hash函數生成一個數字,將這個數字除以number_of_primary_shards(分片的數量)後獲得餘數。這個分佈在0到number_of_primary_shards - 1之間的餘數,就是咱們所尋求的文檔所在分片的位置。這也就說明了一旦分片數定下來就不能再改變的緣由,由於分片數改變以後,全部以前的路由值都會變得無效,前期建立的文檔也就找不到了。
因爲在Elasticsearch集羣中每一個節點都知道集羣中的文檔的存放位置(經過路由公式定位),因此每一個節點都有處理讀寫請求的能力。在一個寫請求被髮送到集羣中的一個節點後,此時,該節點被稱爲協調點(Coordinating Node),協調點會根據路由公式計算出須要寫到哪一個分片上,再將請求轉發到該分片的主分片節點上。寫操做的流程以下(鍵圖5-20,圖片來自官網)。
(1)客戶端向Node 1(協調節點)發送寫請求。
(2)Node 1經過文檔的_id(默認是_id,但不表示必定是_id)肯定文檔屬於哪一個分片(在本例中是編號爲0的分片)。請求會被轉發到主分片所在的節點Node 3上。
(3)Node 3在主分片上執行請求,若是成功,則將請求並行轉發到Node 1和Node 2的副本分片上。一旦全部的副本分片都報告成功(默認),則Node 3將向協調節點報告成功,協調節點向客戶端報告成功。
根據routing字段進行的單個文檔的查詢,在Elasticsearch集羣中能夠在主分片或者副本分片上進行。查詢字段恰好是routing的分片字段如「_id」的查詢流程以下(見圖5-21,圖片來自官網)。
(1)客戶端向集羣發送查詢請求,集羣再隨機選擇一個節點做爲協調點(Node 1),負責處理此次查詢。
(2)Node 1使用文檔的routing id來計算要查詢的文檔在哪一個分片上(在本例中落在了0分片上)分片0的副本分片存在全部的三個節點上。在這種狀況下,協調節點能夠把請求轉發到任意節點,本例將請求轉發到Node 2上。
(3)Node 2執行查找,並將查找結果返回給協調節點Node 1,Node 1再將文檔返回給客戶端。
若是須要給我修改意見的發送郵箱:erghjmncq6643981@163.com
資料參考:《可伸縮服務架構》
轉發博客,請註明,謝謝。
走過路過不要錯過,您的支持是我持續技術輸出的動力所在,金額隨意,感謝!!