Elasticsearch - 搜索引擎Lucene

1.1. Scaling Lucene

怎樣在Lucene之上構建一個分佈式、高度伸縮、接近實時的搜索引擎呢?

讓我們回顧一下在搜索引擎(基於lucene)伸縮性這條路上都做了那些嘗試,並且elasticsearch是如何嘗試並去解決這些挑戰的。

首先我們瞭解下最基礎的理論知識 building blocks (這些理論基礎是構建分佈式近實時搜索引擎的基礎)。 接着我們研究一下到底哪種纔是最佳的分區策略 partitioning (將lucene索引文檔分割到多個分佈式的分片中去)。 然後我們同樣需要決定使用哪種分區複製方式 replication (複製能夠保證系統的高可用以及提高搜索的吞吐)。 最後,我們再看一下事務日誌 transaction log (事務日誌在elasticsearch裏面是一個保證數據一致性的非常酷的功能)。

1.1.1. Building Blocks

當我們要構建一個分佈式接近實時的搜索引擎,並且要讓lucene可伸縮可擴展,必須首先知道lucene的關鍵概念以及它們與我們要達成目標的一些侷限性.

Directory

Lucene Directory 是一個抽象的文件系統的接口,用來允許你讀寫文件,不管lucene的索引是存放在內存中還是在物理磁盤上,它都是通過lucene的Directory抽象層來訪問和維護的。

IndexWriter

IndexWriter 用來添加、刪除和更新lucene裏面的索引文檔。這些操作是在內存中完成以保證更好的性能,但是如果要保證這些操作的持久化,這些操作是需要flush到磁盤的。並且,flush操作或者是顯式的commit提交開銷都是比較大的,因爲這些操作通常需要處理很多的文件,而處理這些文件又涉及到大量的磁盤io。
此外, 每次只能有一個IndexWriter對象來對一個索引目錄進行索引操作,並且創建這個對象的開銷很大,所以必須儘可能去重用這個對象.

Index Segments

Lucene 索引被分解爲很多段(segments)。每個索引段實際上是一個功能完整的lucene索引,一旦一個索引段創建完成,它將是不可變的,並且不能刪除段裏面的索引文檔。commit提交操作用來往索引裏面添加一個新段。lucene內部會來對這些段進行合併,所以我們必須要有策略來控制這些合併(MergePolisy, MergeScheuler, … etc)。Because segments need to be kept at bay they are being merged continuously by internal Lucene processes (MergePolisy, MergeScheuler, … etc)。

因爲段是不可變的,所以用來做緩存(caching)是一個很好的選擇,你可以加載所有的term詞條並且創建一個跳躍列表( skip lists ) ,或者用來構造FieldCache,如果段沒有變化,你就不需要重新加載。

IndexReader

IndexReader 用來執行搜索索引。這個對象通過IndexWriter來提供,並且創建代價也是比較高。一旦IndexReader打開之後,它就不能夠發現打開之後的索引變化,如果要知道這些由IndexWriter產生的索引變化,除非刷新IndexReader對象(當然前提需要flush操作)。搜索操作在內部其實是按段來進行的(每次一個段)。

Near Real-Time Search

獲取一個新的IndexReader開銷很大,所以也是我們不能每次一有索引操作就真的去獲取一個新的IndexReader,你可以隔一段時間去刷新一下,比如每隔一秒鐘等等,這也是我們在這裏稱之爲接近實時( near real-time )的原因。


1.1.2. Partitioning

可能用來伸縮Lucene的途徑(Possible approach to Scale Lucene):

Distributed Directory

其中一個途徑用來伸縮Lucene就是使用分佈式文件系統,大文件會被拆分成chunks塊並且會保存到分佈式存儲系統(比如 Coherence, Terracota, GigaSpaces or Infinispan等等)。這樣IndexWriter和IndexReader都是工作在一個自定義的Directory分佈式實現上,每個操作後面其實是分佈了很多個節點,每個節點上面存儲了索引文件的一部分.

但是這種方案有一些問題:

首先,這種方案會產生密集的網絡流量。儘管可以用一些高級的方法如本地緩存等,但仍然會產生大量的網絡請求,因爲最主要的原因是因爲這種將文件拆分爲塊的想法與lucene索引文件構建方式和使用方式實在相隔太遠,結論就是使用這種方式來做大規模索引和搜索是不切實際的。(ps:所以solandra這種玩意還是不要去考慮了)。

其次,大的索引必然會使IndexReader變的無法分佈式。IndexReader是一個很重的對象,並且term詞條越多,其消耗的內存也會越多。

最後,索引操作也會變的非常困難,因爲只有一個單一的IndexWriter能夠寫索引。所以,我們把目光投向另一種方式。


Partitioning

有2種通過將數據分區方式來scale搜索引擎: 基於文檔(Document based partitioning) and 基於詞條(Term based partitioning). Elasticsearch 使用的基於文檔的分區方式。

基於文檔的分區(Document Based Partitioning)

每一個文檔只存一個分區,每個分區持有整個文檔集的一個子集,分區是一個功能完整的索引。

優點:

每個分區都可以獨立的處理查詢。

可以非常簡單的添加以文檔爲單位的索引信息。

網絡開銷很小,每個節點可以分別執行搜索,執行完了之後只需用返回文檔的ID和評分信息就可以了,然後在其中一個我們執行分佈式搜索的節點上執行合併就可以了。

缺點:

查詢如果需要在所有的分區上執行,那麼它將執行 O(K*N) 次磁盤操作(K是詞條(Term,或者理解爲Field)的數量,N是分區的數量)。

在實用性的角度來看基於文檔的分區方式已經被證明是一個構建大型的分佈式信息檢索系統的一種行之有效的方法, 關於這方面的詳細內容,可以看 這裏 talk by Jeffrey Dean (Google)。

基於詞條的分區(Term Based Partitioning)

每個分區擁有一部分詞條,詞條裏面包含了整個index的文檔數據。

一些基於詞條分區的系統,如Riak Search (built on top of Riak key-value store engine) 或是 Lucandra/Solandra (on top of Cassandra). 儘管這些系統不是完全一樣,但是它們都面臨一個相似的挑戰,當然也得益於相同的設計理念。

優點:

一般來說,你只需要在很少的部分分區上執行查詢就行了,比如,我們有5個term詞條的查詢,我們將至多命中5個分區,如果這5個term詞條都保存同一個分區中,那麼我們只需用訪問一個分區即可,而不管我們是不是實際上有50個分區。

另外一個優勢就是對應K個Term詞條的查詢,你只需用執行 O(K) 次磁盤查找(假設我們使用的優化過的實現)。

缺點:

最主要的問題是Lucene Segment概念裏面固有的很多結構都將失去。

The main problem is that whole notion of Lucene Segment which is inherent to a lot of constructs in Lucene is lost.

對於那些複雜的查詢,網絡開銷將會變得非常高,並且可能使得系統可用性大大降低,尤其是那些會expand出大量的term詞條的查詢,如fuzzy或者prefix查詢。

另外一個問題就是獲取每個文檔的信息將會變得非常困難,舉例來說,如果你想獲取文檔的一部分數據來做進一步的控制,比如(google的PageRank算法),獲取每個文檔的這些數據都會變得非常困難,因爲這種分區的方式使得文檔的數據被分散到了不同的地方,所以實現faceting、評分、自定義評分等等都將變得難以實現。


1.1.3. Replication

分佈式系統的另外一方面就是複製(replication)了。通過複製我們可以得到2個主要的好處:

High Availability (HA高可用性)。如果一個節點掛了,另外一個節點能從它趴下的地方應頭頂上,如果一個節點上面持有索引分片,而另一個節點持有該分片的副本,那麼我們的數據就有了一個備份。

擁有數據多個副本的另一個好處就是 scalability (可伸縮性)。我們沒有理由不通過增加副本來提高搜索能力,而我們只需要簡單的增加幾個副本或從節點(slave nodes)就能提升我們搜索的吞吐,何樂而不爲呢。

一般有兩種方式來實現複製: Push Replication(推模式) 和 Pull Replication(拉模式)。 Elasticsearch 使用的是Push Replication(推模式)。


Push Replication

工作起來非常簡單, 當你往 [master] 主分片上面索引一個文檔,該分片會複製該文檔(document)到剩下的所有 [replica] 副本分片中,這些分片也會索引這個文檔。

缺點:

同一個文檔重複索引多次,相比拉模式而言,要傳輸相對較少的數據(衆所周知,Lucene索引速度非常快)。

You index the same document several times, but we transfer much less data compared to Pull replication (and Lucene is known to index very fast)。

這就需要在併發索引的時候進行一些微妙的控制,比如對同一個文檔進行非常頻繁的索引,在主分片和副本分片上面執行索引操作的時候,你必須保證每次更新是按照正確的順序,或者通過版本(versioning)來拒絕舊版本的操作,而拉模式就沒有這個問題。

優點:

一旦文檔索引完畢,那麼該文檔在所有的分片及副本上都是立即可用的。 索引操作會等待直到確認所有的副本也執行了同樣的索引操作(注意: 如果需要,你也可以使用異步複製)。 這意味着索引的實時性。 然後你只需要 refresh 一下 IndexReader 就能搜索到新的數據了。

這樣的架構能讓你非常方便的在節點之間進行切換,假如包含主分片(primary shard)的節點掛了,我們能夠很快的進行切換,因爲其它的分片和主分片都是一模一樣的。


Pull Replication

拉模式是一種主從方式(master – slave)(Solr 用的就是這種)。 當一個文檔在master上面進行索引,並且數據通過commit操作產生了新的段文件(segment),這個時候,從節點(slave)把這些段文件(segments)拉到自己的機器然後再執行相應的刷新操作,並保證lucene能夠使用這些新的數據。

缺點:

需要在master上面執行commit操作來產生這些段文件(segment),這樣slave才能夠執行pull操作。 不知道你還記不記得前面說過,lucene的commit的開銷是非常大的,如果可能,commit次數越少越好。

數據的傳輸會有不必要的冗餘。 在分佈式系統裏面,網絡通常來說是非常寶貴的資源(如果你跑在EC2上面,那將更加寶貴) 並且最終要移動的數據會越來越多,舉例來說,如果你有2個段文件,裏面包含了文檔,文檔裏面的字段都是存儲的(stored fields),並且Lucene決定要合併這2個段文件,那麼你也必須要傳輸這部分數據(合併之後的段文件),因爲這是一個新的段文件,但是實際上你傳輸的是一份相同的數據。

這將造成一個這樣的局面,所有的slaves確實是在master後面。 也可能是確實沒有理由每次都進行commit或者花大量時間來傳輸一個大的段文件。但是至少意味着你的slave會丟失 high availability,並且不可能當成是一個實時的slave(a real time high available slave)。 實時搜索不可能存在,並且(使用拉模式)也不可能有這種1秒的刷新率,然後lucene就能實時搜索。


1.1.4. Transaction Log

正如前面提到過的,索引提交(commit)的開銷實在太大,但是我們又必須通過提交操作來保證數據被可靠的持久化,如果擁有數據的節點突然崩潰的話,那麼最後一次提交操作之後產生的數據操作將會丟失。


數據可靠性(Data Persistency)

ElasticSearch通過使用 transaction log (或預寫日誌(write ahead log)) 來解決這個問題,通過日誌記錄發生在索引上的各種操作,來保證就算沒有調用commit操作也能保證數據的持久化。並且能夠很自然的支持推送複製(push replication),因爲我們能夠讓每個不同的shard都擁有 transaction log ,就算某些節點崩潰了,如果有必要,可以很輕鬆對日誌操作進行重放(replay)。

Transaction log 週期性的將數據刷新(flushed)到磁盤,你可以通過 參數 來進行控制。 簡單來說就是保存兩次提交之間的連續數據操作的記錄。

儘管你只運行了一個elasticsearch的服務節點(可能暫時不需要分佈式),trasncation log也能夠使你的es即使被強制結束進程( “kill -9” )也不會丟失任何數據。

當然,還不止這些!Transaction log還有一個重要的功能就是可以保證當你生成快照( shared gateway snapshot )、分片恢復( peer shard recovery )或是分片熱遷移(shard “Hot” relocation)的時候,索引數據不會丟失。


Shared Gateway Snapshot

使用共享gateway時,會週期性的生成數據改變(changes)的快照 ( snapshots ) ,並存儲到共享存儲中(shared storage),並且transaction log也是持久化數據的一部分。


Peer Shard Reovery

當分片從一個節點遷移到另一個節點或者需要分配更多的分片(比如你 增加 了副本數) 的時候,數據會從某一個節點上取來進行恢復,而不是從gateway。

遷移數據時,首先我們保證不會刪除Lucene的段文件(segment files),然後禁用flushing操作,這個時候保證不調用commit操作,然後開始遷移這些段文件,這個時候產生的索引改變,我們存放到transaction log中,一旦這個步驟結束(ie:索引索引文件拷貝完畢),我們開始對transaction log裏面的日誌在replica分片上進行重放操作(replay),完畢之後,我們就可以進行切換了,數據遷移成功!

遷移操作進行時,你仍然可以進行索引,仍然可以進行搜索,只有索引切換的時候會有一段很短的時間阻塞(blocking),但是直到切換前,遷移對你來說是完全透明的。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章